feat(wayland): integrate the built-in XWM with the server
Boot runs on the starter goroutine with a context that ends when the Xwayland process dies, and installXWM/stop run on the display goroutine. hostImpl maps XWM windows to neferwl windows (x11 role, Positioned map events), the xwayland_shell_v1 global is visible to the Xwayland client only, and configure/place/focus/close are forwarded to the XWM.
- commit
- 24f47eb83d49568d4a4d3eaab22c7ae000da3dd0
- author
- brice · · 2026-10-22 13:23 UTC
- committer
- brice · · 2026-10-22 17:54 UTC
- parent
- e1de5a5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
21 files · +1186 -75
M internal/adapters/wayland/dmabuf.go +3 -2
| @@ -223,10 +223,11 @@ func (g *dmabufGlobal) fullscreenOutput(surf *surface) (string, bool) { | |||
| 223 | 223 | return "", false | |
| 224 | 224 | } | |
| 225 | 225 | root := toplevelRoot(surf) | |
| 226 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen || g.server.invisible(surf) { | |
| 226 | + | w := root.toplevelWindow() | |
| 227 | + | if w == nil || !w.hasLast || !w.last.Fullscreen || g.server.invisible(surf) { | |
| 227 | 228 | return "", false | |
| 228 | 229 | } | |
| 229 | - | return root.xdg.window.last.Output, true | |
| 230 | + | return w.last.Output, true | |
| 230 | 231 | } | |
| 231 | 232 | ||
| 232 | 233 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | |
| @@ -223,10 +223,11 @@ func (g *dmabufGlobal) fullscreenOutput(surf *surface) (string, bool) { | |||||
| 223 | return "", false | 223 | return "", false | ||
| 224 | } | 224 | } | ||
| 225 | root := toplevelRoot(surf) | 225 | root := toplevelRoot(surf) | ||
| 226 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen || g.server.invisible(surf) { | 226 | + | w := root.toplevelWindow() |
| 227 | + | if w == nil || !w.hasLast || !w.last.Fullscreen || g.server.invisible(surf) { | |||
| 227 | return "", false | 228 | return "", false | ||
| 228 | } | 229 | } | ||
| 229 | - | return root.xdg.window.last.Output, true | 230 | + | return w.last.Output, true |
| 230 | } | 231 | } | ||
| 231 | 232 | ||||
| 232 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | 233 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | ||
M internal/adapters/wayland/frames.go +7 -5
| @@ -193,7 +193,8 @@ func toplevelRoot(surf *surface) *surface { | |||
| 193 | 193 | // for a capture session is invisible. | |
| 194 | 194 | func (s *Server) invisible(surf *surface) bool { | |
| 195 | 195 | root := toplevelRoot(surf) | |
| 196 | - | return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible | |
| 196 | + | w := root.toplevelWindow() | |
| 197 | + | return w != nil && w.hasLast && !w.last.Visible | |
| 197 | 198 | } | |
| 198 | 199 | ||
| 199 | 200 | // suspended reports that nothing draws the toplevel tree, not even a capture | |
| @@ -201,10 +202,11 @@ func (s *Server) invisible(surf *surface) bool { | |||
| 201 | 202 | // frame callbacks of its output so the capture keeps moving. | |
| 202 | 203 | func (s *Server) suspended(surf *surface) bool { | |
| 203 | 204 | root := toplevelRoot(surf) | |
| 204 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast { | |
| 205 | + | w := root.toplevelWindow() | |
| 206 | + | if w == nil || !w.hasLast { | |
| 205 | 207 | return false | |
| 206 | 208 | } | |
| 207 | - | return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured | |
| 209 | + | return !w.last.Visible && !w.last.Captured | |
| 208 | 210 | } | |
| 209 | 211 | ||
| 210 | 212 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | |
| @@ -212,11 +214,11 @@ func (s *Server) suspended(surf *surface) bool { | |||
| 212 | 214 | func (s *Server) frameOutput(surf *surface) string { | |
| 213 | 215 | surf = toplevelRoot(surf) | |
| 214 | 216 | switch { | |
| 215 | - | case surf.xdg != nil && surf.xdg.window != nil: | |
| 217 | + | case surf.toplevelWindow() != nil: | |
| 216 | 218 | if s.suspended(surf) { | |
| 217 | 219 | return suspendedFrameQueue | |
| 218 | 220 | } | |
| 219 | - | return surf.xdg.window.last.Output | |
| 221 | + | return surf.toplevelWindow().last.Output | |
| 220 | 222 | case surf.layer != nil && surf.layer.output != nil: | |
| 221 | 223 | return surf.layer.output.name() | |
| 222 | 224 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | |
| @@ -193,7 +193,8 @@ func toplevelRoot(surf *surface) *surface { | |||||
| 193 | // for a capture session is invisible. | 193 | // for a capture session is invisible. | ||
| 194 | func (s *Server) invisible(surf *surface) bool { | 194 | func (s *Server) invisible(surf *surface) bool { | ||
| 195 | root := toplevelRoot(surf) | 195 | root := toplevelRoot(surf) | ||
| 196 | - | return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible | 196 | + | w := root.toplevelWindow() |
| 197 | + | return w != nil && w.hasLast && !w.last.Visible | |||
| 197 | } | 198 | } | ||
| 198 | 199 | ||||
| 199 | // suspended reports that nothing draws the toplevel tree, not even a capture | 200 | // suspended reports that nothing draws the toplevel tree, not even a capture | ||
| @@ -201,10 +202,11 @@ func (s *Server) invisible(surf *surface) bool { | |||||
| 201 | // frame callbacks of its output so the capture keeps moving. | 202 | // frame callbacks of its output so the capture keeps moving. | ||
| 202 | func (s *Server) suspended(surf *surface) bool { | 203 | func (s *Server) suspended(surf *surface) bool { | ||
| 203 | root := toplevelRoot(surf) | 204 | root := toplevelRoot(surf) | ||
| 204 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast { | 205 | + | w := root.toplevelWindow() |
| 206 | + | if w == nil || !w.hasLast { | |||
| 205 | return false | 207 | return false | ||
| 206 | } | 208 | } | ||
| 207 | - | return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured | 209 | + | return !w.last.Visible && !w.last.Captured |
| 208 | } | 210 | } | ||
| 209 | 211 | ||||
| 210 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | 212 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | ||
| @@ -212,11 +214,11 @@ func (s *Server) suspended(surf *surface) bool { | |||||
| 212 | func (s *Server) frameOutput(surf *surface) string { | 214 | func (s *Server) frameOutput(surf *surface) string { | ||
| 213 | surf = toplevelRoot(surf) | 215 | surf = toplevelRoot(surf) | ||
| 214 | switch { | 216 | switch { | ||
| 215 | - | case surf.xdg != nil && surf.xdg.window != nil: | 217 | + | case surf.toplevelWindow() != nil: |
| 216 | if s.suspended(surf) { | 218 | if s.suspended(surf) { | ||
| 217 | return suspendedFrameQueue | 219 | return suspendedFrameQueue | ||
| 218 | } | 220 | } | ||
| 219 | - | return surf.xdg.window.last.Output | 221 | + | return surf.toplevelWindow().last.Output |
| 220 | case surf.layer != nil && surf.layer.output != nil: | 222 | case surf.layer != nil && surf.layer.output != nil: | ||
| 221 | return surf.layer.output.name() | 223 | return surf.layer.output.name() | ||
| 222 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | 224 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | ||
M internal/adapters/wayland/globals.go +4 -0
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | func registerGlobals(d *server.Display, o Options, s *Server) error { | |
| 14 | 14 | for _, register := range []func() error{ | |
| 15 | 15 | func() error { return registerXDG(d, s) }, | |
| 16 | + | func() error { return registerXwaylandShell(d, s) }, | |
| 16 | 17 | func() error { return registerLayer(d, s) }, | |
| 17 | 18 | func() error { | |
| 18 | 19 | // Advertise only with the production owner gate and authoritative | |
| @@ -137,6 +138,9 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||
| 137 | 138 | delete(c.server.surfaces, w.Resource) | |
| 138 | 139 | delete(c.server.surfaceByIdentity, state.identity) | |
| 139 | 140 | state.Destroy(w) | |
| 141 | + | if state.kind == roleX11 && c.server.xwm != nil { | |
| 142 | + | c.server.xwm.SurfaceGone(state.identity) | |
| 143 | + | } | |
| 140 | 144 | } | |
| 141 | 145 | } | |
| 142 | 146 | } | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | func registerGlobals(d *server.Display, o Options, s *Server) error { | 13 | func registerGlobals(d *server.Display, o Options, s *Server) error { | ||
| 14 | for _, register := range []func() error{ | 14 | for _, register := range []func() error{ | ||
| 15 | func() error { return registerXDG(d, s) }, | 15 | func() error { return registerXDG(d, s) }, | ||
| 16 | + | func() error { return registerXwaylandShell(d, s) }, | |||
| 16 | func() error { return registerLayer(d, s) }, | 17 | func() error { return registerLayer(d, s) }, | ||
| 17 | func() error { | 18 | func() error { | ||
| 18 | // Advertise only with the production owner gate and authoritative | 19 | // Advertise only with the production owner gate and authoritative | ||
| @@ -137,6 +138,9 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||||
| 137 | delete(c.server.surfaces, w.Resource) | 138 | delete(c.server.surfaces, w.Resource) | ||
| 138 | delete(c.server.surfaceByIdentity, state.identity) | 139 | delete(c.server.surfaceByIdentity, state.identity) | ||
| 139 | state.Destroy(w) | 140 | state.Destroy(w) | ||
| 141 | + | if state.kind == roleX11 && c.server.xwm != nil { | |||
| 142 | + | c.server.xwm.SurfaceGone(state.identity) | |||
| 143 | + | } | |||
| 140 | } | 144 | } | ||
| 141 | } | 145 | } | ||
| 142 | } | 146 | } | ||
M internal/adapters/wayland/scale.go +2 -2
| @@ -25,8 +25,8 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||
| 25 | 25 | } | |
| 26 | 26 | name := "" | |
| 27 | 27 | switch { | |
| 28 | - | case surf.xdg != nil && surf.xdg.window != nil: | |
| 29 | - | name = surf.xdg.window.last.Output | |
| 28 | + | case surf.toplevelWindow() != nil: | |
| 29 | + | name = surf.toplevelWindow().last.Output | |
| 30 | 30 | case surf.layer != nil && surf.layer.output != nil: | |
| 31 | 31 | return surf.layer.output | |
| 32 | 32 | case surf.lock != nil: | |
| @@ -25,8 +25,8 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||||
| 25 | } | 25 | } | ||
| 26 | name := "" | 26 | name := "" | ||
| 27 | switch { | 27 | switch { | ||
| 28 | - | case surf.xdg != nil && surf.xdg.window != nil: | 28 | + | case surf.toplevelWindow() != nil: |
| 29 | - | name = surf.xdg.window.last.Output | 29 | + | name = surf.toplevelWindow().last.Output |
| 30 | case surf.layer != nil && surf.layer.output != nil: | 30 | case surf.layer != nil && surf.layer.output != nil: | ||
| 31 | return surf.layer.output | 31 | return surf.layer.output | ||
| 32 | case surf.lock != nil: | 32 | case surf.lock != nil: | ||
M internal/adapters/wayland/seat.go +1 -0
| @@ -455,6 +455,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||
| 455 | 455 | } | |
| 456 | 456 | } | |
| 457 | 457 | s.textInputFocus() | |
| 458 | + | s.x11Focus() | |
| 458 | 459 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | |
| 459 | 460 | s.updateConstraint() | |
| 460 | 461 | } | |
| @@ -455,6 +455,7 @@ func (s *Server) changeFocus(id ports.WindowID) { | |||||
| 455 | } | 455 | } | ||
| 456 | } | 456 | } | ||
| 457 | s.textInputFocus() | 457 | s.textInputFocus() | ||
| 458 | + | s.x11Focus() | |||
| 458 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | 459 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | ||
| 459 | s.updateConstraint() | 460 | s.updateConstraint() | ||
| 460 | } | 461 | } | ||
M internal/adapters/wayland/server.go +21 -9
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | |
| 17 | 17 | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 18 | 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | |
| 19 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 19 | 20 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | |
| 20 | 21 | "github.com/bnema/neferwl/internal/ports" | |
| 21 | 22 | "github.com/bnema/purego-libwayland/server" | |
| @@ -160,14 +161,18 @@ type Server struct { | |||
| 160 | 161 | securityStop int | |
| 161 | 162 | securityWG sync.WaitGroup | |
| 162 | 163 | securityLive atomic.Int32 | |
| 163 | - | xwayland xwaylandClient // display owner; Phase 6 takes the XWM fd from here. | |
| 164 | - | display *server.Display | |
| 165 | - | env procEnv | |
| 166 | - | name string | |
| 167 | - | cleanup func() | |
| 168 | - | log zerowrap.Logger | |
| 169 | - | logDamage bool | |
| 170 | - | channels Channels | |
| 164 | + | xwayland xwaylandClient // display owner | |
| 165 | + | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its | |
| 166 | + | // slots to their windows. Display goroutine only. | |
| 167 | + | xwm *xwm.XWM | |
| 168 | + | x11Slots map[xwm.Slot]*window | |
| 169 | + | display *server.Display | |
| 170 | + | env procEnv | |
| 171 | + | name string | |
| 172 | + | cleanup func() | |
| 173 | + | log zerowrap.Logger | |
| 174 | + | logDamage bool | |
| 175 | + | channels Channels | |
| 171 | 176 | // awaiting holds frame callbacks by output name, due at its next | |
| 172 | 177 | // frame (frameDue, or its page flip). | |
| 173 | 178 | awaiting map[string][]*wayland.Callback | |
| @@ -373,6 +378,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 373 | 378 | surfaceByIdentity: make(map[uint64]*surface), | |
| 374 | 379 | buffers: make(map[*server.Resource]clientBuffer), | |
| 375 | 380 | windows: make(map[ports.WindowID]*window), | |
| 381 | + | x11Slots: make(map[xwm.Slot]*window), | |
| 376 | 382 | layers: make(map[ports.WindowID]*layerSurface), | |
| 377 | 383 | nextWindow: 1, | |
| 378 | 384 | eventReady: make(chan struct{}, 1), | |
| @@ -503,6 +509,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 503 | 509 | if err = s.openPaceTimer(); err != nil { | |
| 504 | 510 | return abort(err) | |
| 505 | 511 | } | |
| 512 | + | d.SetGlobalFilter(s.globalVisible) | |
| 506 | 513 | if err = registerGlobals(d, opts, s); err != nil { | |
| 507 | 514 | return abort(err) | |
| 508 | 515 | } | |
| @@ -590,6 +597,11 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 590 | 597 | } | |
| 591 | 598 | s.castInventoryChanged() | |
| 592 | 599 | err := s.display.Run(ctx) | |
| 600 | + | // The display is gone: only the X connection is left to close. | |
| 601 | + | if s.xwm != nil { | |
| 602 | + | s.xwm.CloseConn() | |
| 603 | + | s.xwm = nil | |
| 604 | + | } | |
| 593 | 605 | s.forgetXwayland() | |
| 594 | 606 | s.stopSecurityContexts() | |
| 595 | 607 | wg.Wait() | |
| @@ -781,7 +793,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 781 | 793 | w.popup.dismiss() | |
| 782 | 794 | } | |
| 783 | 795 | case ports.PlaceWindow: | |
| 784 | - | // Consumed by the X11 host. | |
| 796 | + | s.x11Place(c) | |
| 785 | 797 | case ports.ConfigureWindow: | |
| 786 | 798 | w := s.windows[c.ID] | |
| 787 | 799 | if w == nil || !w.toplevelAlive() || !w.alive() { | |
| @@ -16,6 +16,7 @@ import ( | |||||
| 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | ||
| 17 | "github.com/bnema/neferwl/internal/adapters/clock" | 17 | "github.com/bnema/neferwl/internal/adapters/clock" | ||
| 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | ||
| 19 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 19 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | 20 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | ||
| 20 | "github.com/bnema/neferwl/internal/ports" | 21 | "github.com/bnema/neferwl/internal/ports" | ||
| 21 | "github.com/bnema/purego-libwayland/server" | 22 | "github.com/bnema/purego-libwayland/server" | ||
| @@ -160,14 +161,18 @@ type Server struct { | |||||
| 160 | securityStop int | 161 | securityStop int | ||
| 161 | securityWG sync.WaitGroup | 162 | securityWG sync.WaitGroup | ||
| 162 | securityLive atomic.Int32 | 163 | securityLive atomic.Int32 | ||
| 163 | - | xwayland xwaylandClient // display owner; Phase 6 takes the XWM fd from here. | 164 | + | xwayland xwaylandClient // display owner |
| 164 | - | display *server.Display | 165 | + | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its |
| 165 | - | env procEnv | 166 | + | // slots to their windows. Display goroutine only. |
| 166 | - | name string | 167 | + | xwm *xwm.XWM |
| 167 | - | cleanup func() | 168 | + | x11Slots map[xwm.Slot]*window |
| 168 | - | log zerowrap.Logger | 169 | + | display *server.Display |
| 169 | - | logDamage bool | 170 | + | env procEnv |
| 170 | - | channels Channels | 171 | + | name string |
| 172 | + | cleanup func() | |||
| 173 | + | log zerowrap.Logger | |||
| 174 | + | logDamage bool | |||
| 175 | + | channels Channels | |||
| 171 | // awaiting holds frame callbacks by output name, due at its next | 176 | // awaiting holds frame callbacks by output name, due at its next | ||
| 172 | // frame (frameDue, or its page flip). | 177 | // frame (frameDue, or its page flip). | ||
| 173 | awaiting map[string][]*wayland.Callback | 178 | awaiting map[string][]*wayland.Callback | ||
| @@ -373,6 +378,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 373 | surfaceByIdentity: make(map[uint64]*surface), | 378 | surfaceByIdentity: make(map[uint64]*surface), | ||
| 374 | buffers: make(map[*server.Resource]clientBuffer), | 379 | buffers: make(map[*server.Resource]clientBuffer), | ||
| 375 | windows: make(map[ports.WindowID]*window), | 380 | windows: make(map[ports.WindowID]*window), | ||
| 381 | + | x11Slots: make(map[xwm.Slot]*window), | |||
| 376 | layers: make(map[ports.WindowID]*layerSurface), | 382 | layers: make(map[ports.WindowID]*layerSurface), | ||
| 377 | nextWindow: 1, | 383 | nextWindow: 1, | ||
| 378 | eventReady: make(chan struct{}, 1), | 384 | eventReady: make(chan struct{}, 1), | ||
| @@ -503,6 +509,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 503 | if err = s.openPaceTimer(); err != nil { | 509 | if err = s.openPaceTimer(); err != nil { | ||
| 504 | return abort(err) | 510 | return abort(err) | ||
| 505 | } | 511 | } | ||
| 512 | + | d.SetGlobalFilter(s.globalVisible) | |||
| 506 | if err = registerGlobals(d, opts, s); err != nil { | 513 | if err = registerGlobals(d, opts, s); err != nil { | ||
| 507 | return abort(err) | 514 | return abort(err) | ||
| 508 | } | 515 | } | ||
| @@ -590,6 +597,11 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 590 | } | 597 | } | ||
| 591 | s.castInventoryChanged() | 598 | s.castInventoryChanged() | ||
| 592 | err := s.display.Run(ctx) | 599 | err := s.display.Run(ctx) | ||
| 600 | + | // The display is gone: only the X connection is left to close. | |||
| 601 | + | if s.xwm != nil { | |||
| 602 | + | s.xwm.CloseConn() | |||
| 603 | + | s.xwm = nil | |||
| 604 | + | } | |||
| 593 | s.forgetXwayland() | 605 | s.forgetXwayland() | ||
| 594 | s.stopSecurityContexts() | 606 | s.stopSecurityContexts() | ||
| 595 | wg.Wait() | 607 | wg.Wait() | ||
| @@ -781,7 +793,7 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 781 | w.popup.dismiss() | 793 | w.popup.dismiss() | ||
| 782 | } | 794 | } | ||
| 783 | case ports.PlaceWindow: | 795 | case ports.PlaceWindow: | ||
| 784 | - | // Consumed by the X11 host. | 796 | + | s.x11Place(c) |
| 785 | case ports.ConfigureWindow: | 797 | case ports.ConfigureWindow: | ||
| 786 | w := s.windows[c.ID] | 798 | w := s.windows[c.ID] | ||
| 787 | if w == nil || !w.toplevelAlive() || !w.alive() { | 799 | if w == nil || !w.toplevelAlive() || !w.alive() { | ||
M internal/adapters/wayland/surface.go +12 -3
| @@ -30,7 +30,6 @@ const ( | |||
| 30 | 30 | roleDragIcon | |
| 31 | 31 | // roleX11 is the surface of an X11 window paired through | |
| 32 | 32 | // xwayland_shell_v1. | |
| 33 | - | //lint:ignore U1000 used by the X11 window role (xwm) | |
| 34 | 33 | roleX11 | |
| 35 | 34 | ) | |
| 36 | 35 | ||
| @@ -51,8 +50,12 @@ type surface struct { | |||
| 51 | 50 | formatOpaque bool | |
| 52 | 51 | kind roleKind | |
| 53 | 52 | xdg *xdgSurface | |
| 54 | - | layer *layerSurface | |
| 55 | - | lock *lockSurface | |
| 53 | + | // x11win is the X11 window the surface is associated with; x11Serial is | |
| 54 | + | // the serial of xwayland_surface_v1.set_serial, 0 until set. | |
| 55 | + | x11win *window | |
| 56 | + | x11Serial uint64 | |
| 57 | + | layer *layerSurface | |
| 58 | + | lock *lockSurface | |
| 56 | 59 | // bufferCommitted is permanent, including commits later discarded by FIFO. | |
| 57 | 60 | bufferCommitted bool | |
| 58 | 61 | server *Server | |
| @@ -196,6 +199,9 @@ func (s *surface) windowID() ports.WindowID { | |||
| 196 | 199 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | |
| 197 | 200 | return s.xdg.window.id | |
| 198 | 201 | } | |
| 202 | + | if s.x11win != nil && s.x11win.mapped { | |
| 203 | + | return s.x11win.id | |
| 204 | + | } | |
| 199 | 205 | if s.layer != nil && s.layer.mapped { | |
| 200 | 206 | return s.layer.id | |
| 201 | 207 | } | |
| @@ -570,6 +576,9 @@ func (s *surface) applyCommit(u *update) { | |||
| 570 | 576 | if s.xdg != nil && s.xdg.window != nil { | |
| 571 | 577 | s.xdg.window.afterCommit() | |
| 572 | 578 | } | |
| 579 | + | if s.x11win != nil { | |
| 580 | + | s.x11win.afterCommit() | |
| 581 | + | } | |
| 573 | 582 | if s.lock != nil { | |
| 574 | 583 | // Role callbacks precede content construction; publish lock placement | |
| 575 | 584 | // only once validated current dimensions are available. | |
| @@ -30,7 +30,6 @@ const ( | |||||
| 30 | roleDragIcon | 30 | roleDragIcon | ||
| 31 | // roleX11 is the surface of an X11 window paired through | 31 | // roleX11 is the surface of an X11 window paired through | ||
| 32 | // xwayland_shell_v1. | 32 | // xwayland_shell_v1. | ||
| 33 | - | //lint:ignore U1000 used by the X11 window role (xwm) | |||
| 34 | roleX11 | 33 | roleX11 | ||
| 35 | ) | 34 | ) | ||
| 36 | 35 | ||||
| @@ -51,8 +50,12 @@ type surface struct { | |||||
| 51 | formatOpaque bool | 50 | formatOpaque bool | ||
| 52 | kind roleKind | 51 | kind roleKind | ||
| 53 | xdg *xdgSurface | 52 | xdg *xdgSurface | ||
| 54 | - | layer *layerSurface | 53 | + | // x11win is the X11 window the surface is associated with; x11Serial is |
| 55 | - | lock *lockSurface | 54 | + | // the serial of xwayland_surface_v1.set_serial, 0 until set. |
| 55 | + | x11win *window | |||
| 56 | + | x11Serial uint64 | |||
| 57 | + | layer *layerSurface | |||
| 58 | + | lock *lockSurface | |||
| 56 | // bufferCommitted is permanent, including commits later discarded by FIFO. | 59 | // bufferCommitted is permanent, including commits later discarded by FIFO. | ||
| 57 | bufferCommitted bool | 60 | bufferCommitted bool | ||
| 58 | server *Server | 61 | server *Server | ||
| @@ -196,6 +199,9 @@ func (s *surface) windowID() ports.WindowID { | |||||
| 196 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | 199 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | ||
| 197 | return s.xdg.window.id | 200 | return s.xdg.window.id | ||
| 198 | } | 201 | } | ||
| 202 | + | if s.x11win != nil && s.x11win.mapped { | |||
| 203 | + | return s.x11win.id | |||
| 204 | + | } | |||
| 199 | if s.layer != nil && s.layer.mapped { | 205 | if s.layer != nil && s.layer.mapped { | ||
| 200 | return s.layer.id | 206 | return s.layer.id | ||
| 201 | } | 207 | } | ||
| @@ -570,6 +576,9 @@ func (s *surface) applyCommit(u *update) { | |||||
| 570 | if s.xdg != nil && s.xdg.window != nil { | 576 | if s.xdg != nil && s.xdg.window != nil { | ||
| 571 | s.xdg.window.afterCommit() | 577 | s.xdg.window.afterCommit() | ||
| 572 | } | 578 | } | ||
| 579 | + | if s.x11win != nil { | |||
| 580 | + | s.x11win.afterCommit() | |||
| 581 | + | } | |||
| 573 | if s.lock != nil { | 582 | if s.lock != nil { | ||
| 574 | // Role callbacks precede content construction; publish lock placement | 583 | // Role callbacks precede content construction; publish lock placement | ||
| 575 | // only once validated current dimensions are available. | 584 | // only once validated current dimensions are available. | ||
M internal/adapters/wayland/window.go +49 -8
| @@ -10,26 +10,46 @@ import ( | |||
| 10 | 10 | // them instead of dereferencing w.xdg, which is nil for windows of another | |
| 11 | 11 | // role. | |
| 12 | 12 | ||
| 13 | - | // isToplevel reports whether w is a toplevel (not a popup). | |
| 14 | - | func (w *window) isToplevel() bool { return w.toplevel != nil } | |
| 13 | + | // isToplevel reports whether w is a toplevel (xdg or X11), not a popup. | |
| 14 | + | func (w *window) isToplevel() bool { return w.toplevel != nil || w.x11 != nil } | |
| 15 | + | ||
| 16 | + | // srv is the server the window belongs to. | |
| 17 | + | func (w *window) srv() *Server { | |
| 18 | + | if w.xdg != nil { | |
| 19 | + | return w.xdg.server | |
| 20 | + | } | |
| 21 | + | return w.x11.server | |
| 22 | + | } | |
| 15 | 23 | ||
| 16 | 24 | // surf is the window's surface, nil when it has none. | |
| 17 | 25 | func (w *window) surf() *surface { | |
| 18 | - | if w.xdg != nil { | |
| 26 | + | switch { | |
| 27 | + | case w.xdg != nil: | |
| 19 | 28 | return w.xdg.surface | |
| 29 | + | case w.x11 != nil: | |
| 30 | + | return w.x11.surface | |
| 20 | 31 | } | |
| 21 | 32 | return nil | |
| 22 | 33 | } | |
| 23 | 34 | ||
| 24 | 35 | // alive reports whether the window's role object is alive. | |
| 25 | 36 | func (w *window) alive() bool { | |
| 26 | - | return w.xdg != nil && w.xdg.resource.Resource.Alive() | |
| 37 | + | switch { | |
| 38 | + | case w.xdg != nil: | |
| 39 | + | return w.xdg.resource.Resource.Alive() | |
| 40 | + | case w.x11 != nil: | |
| 41 | + | return w.x11.surface != nil && w.x11.surface.wl.Resource.Alive() | |
| 42 | + | } | |
| 43 | + | return false | |
| 27 | 44 | } | |
| 28 | 45 | ||
| 29 | 46 | // client is the window's client; the zero Client when it has no role object. | |
| 30 | 47 | func (w *window) client() server.Client { | |
| 31 | - | if w.xdg != nil { | |
| 48 | + | switch { | |
| 49 | + | case w.xdg != nil: | |
| 32 | 50 | return w.xdg.resource.Client() | |
| 51 | + | case w.x11 != nil && w.x11.surface != nil: | |
| 52 | + | return w.x11.surface.wl.Client() | |
| 33 | 53 | } | |
| 34 | 54 | return server.Client{} | |
| 35 | 55 | } | |
| @@ -44,28 +64,49 @@ func (w *window) geometry() ports.Rect { | |||
| 44 | 64 | ||
| 45 | 65 | // sendConfigure sends w.last to the client. | |
| 46 | 66 | func (w *window) sendConfigure() { | |
| 47 | - | if w.xdg != nil { | |
| 67 | + | switch { | |
| 68 | + | case w.xdg != nil: | |
| 48 | 69 | w.sendXDGConfigure() | |
| 70 | + | case w.x11 != nil: | |
| 71 | + | w.x11.server.x11Configure(w) | |
| 49 | 72 | } | |
| 50 | 73 | } | |
| 51 | 74 | ||
| 52 | 75 | // sendClose asks the client to close the window. | |
| 53 | 76 | func (w *window) sendClose() { | |
| 54 | - | if w.toplevel != nil { | |
| 77 | + | switch { | |
| 78 | + | case w.toplevel != nil: | |
| 55 | 79 | w.toplevel.SendClose() | |
| 80 | + | case w.x11 != nil: | |
| 81 | + | w.x11.server.x11Close(w) | |
| 56 | 82 | } | |
| 57 | 83 | } | |
| 58 | 84 | ||
| 59 | 85 | // toplevelAlive reports whether the window is a toplevel whose protocol | |
| 60 | 86 | // object is alive. | |
| 61 | 87 | func (w *window) toplevelAlive() bool { | |
| 88 | + | if w.x11 != nil { | |
| 89 | + | return w.alive() | |
| 90 | + | } | |
| 62 | 91 | return w.toplevel != nil && w.toplevel.Resource.Alive() | |
| 63 | 92 | } | |
| 64 | 93 | ||
| 65 | 94 | // surfaceResource is the window's wl_surface, nil when it has none or it died. | |
| 66 | 95 | func (w *window) surfaceResource() *wayland.Surface { | |
| 67 | - | if w.xdg != nil { | |
| 96 | + | switch { | |
| 97 | + | case w.xdg != nil: | |
| 68 | 98 | return w.xdg.surfaceResource() | |
| 99 | + | case w.x11 != nil && w.alive(): | |
| 100 | + | return w.x11.surface.wl | |
| 69 | 101 | } | |
| 70 | 102 | return nil | |
| 71 | 103 | } | |
| 104 | + | ||
| 105 | + | // toplevelWindow is the toplevel window (xdg or X11) the surface is the role | |
| 106 | + | // surface of, nil for other roles and popups' parents are not followed. | |
| 107 | + | func (s *surface) toplevelWindow() *window { | |
| 108 | + | if s.xdg != nil && s.xdg.window != nil { | |
| 109 | + | return s.xdg.window | |
| 110 | + | } | |
| 111 | + | return s.x11win | |
| 112 | + | } | |
| @@ -10,26 +10,46 @@ import ( | |||||
| 10 | // them instead of dereferencing w.xdg, which is nil for windows of another | 10 | // them instead of dereferencing w.xdg, which is nil for windows of another | ||
| 11 | // role. | 11 | // role. | ||
| 12 | 12 | ||||
| 13 | - | // isToplevel reports whether w is a toplevel (not a popup). | 13 | + | // isToplevel reports whether w is a toplevel (xdg or X11), not a popup. |
| 14 | - | func (w *window) isToplevel() bool { return w.toplevel != nil } | 14 | + | func (w *window) isToplevel() bool { return w.toplevel != nil || w.x11 != nil } |
| 15 | + | ||||
| 16 | + | // srv is the server the window belongs to. | |||
| 17 | + | func (w *window) srv() *Server { | |||
| 18 | + | if w.xdg != nil { | |||
| 19 | + | return w.xdg.server | |||
| 20 | + | } | |||
| 21 | + | return w.x11.server | |||
| 22 | + | } | |||
| 15 | 23 | ||||
| 16 | // surf is the window's surface, nil when it has none. | 24 | // surf is the window's surface, nil when it has none. | ||
| 17 | func (w *window) surf() *surface { | 25 | func (w *window) surf() *surface { | ||
| 18 | - | if w.xdg != nil { | 26 | + | switch { |
| 27 | + | case w.xdg != nil: | |||
| 19 | return w.xdg.surface | 28 | return w.xdg.surface | ||
| 29 | + | case w.x11 != nil: | |||
| 30 | + | return w.x11.surface | |||
| 20 | } | 31 | } | ||
| 21 | return nil | 32 | return nil | ||
| 22 | } | 33 | } | ||
| 23 | 34 | ||||
| 24 | // alive reports whether the window's role object is alive. | 35 | // alive reports whether the window's role object is alive. | ||
| 25 | func (w *window) alive() bool { | 36 | func (w *window) alive() bool { | ||
| 26 | - | return w.xdg != nil && w.xdg.resource.Resource.Alive() | 37 | + | switch { |
| 38 | + | case w.xdg != nil: | |||
| 39 | + | return w.xdg.resource.Resource.Alive() | |||
| 40 | + | case w.x11 != nil: | |||
| 41 | + | return w.x11.surface != nil && w.x11.surface.wl.Resource.Alive() | |||
| 42 | + | } | |||
| 43 | + | return false | |||
| 27 | } | 44 | } | ||
| 28 | 45 | ||||
| 29 | // client is the window's client; the zero Client when it has no role object. | 46 | // client is the window's client; the zero Client when it has no role object. | ||
| 30 | func (w *window) client() server.Client { | 47 | func (w *window) client() server.Client { | ||
| 31 | - | if w.xdg != nil { | 48 | + | switch { |
| 49 | + | case w.xdg != nil: | |||
| 32 | return w.xdg.resource.Client() | 50 | return w.xdg.resource.Client() | ||
| 51 | + | case w.x11 != nil && w.x11.surface != nil: | |||
| 52 | + | return w.x11.surface.wl.Client() | |||
| 33 | } | 53 | } | ||
| 34 | return server.Client{} | 54 | return server.Client{} | ||
| 35 | } | 55 | } | ||
| @@ -44,28 +64,49 @@ func (w *window) geometry() ports.Rect { | |||||
| 44 | 64 | ||||
| 45 | // sendConfigure sends w.last to the client. | 65 | // sendConfigure sends w.last to the client. | ||
| 46 | func (w *window) sendConfigure() { | 66 | func (w *window) sendConfigure() { | ||
| 47 | - | if w.xdg != nil { | 67 | + | switch { |
| 68 | + | case w.xdg != nil: | |||
| 48 | w.sendXDGConfigure() | 69 | w.sendXDGConfigure() | ||
| 70 | + | case w.x11 != nil: | |||
| 71 | + | w.x11.server.x11Configure(w) | |||
| 49 | } | 72 | } | ||
| 50 | } | 73 | } | ||
| 51 | 74 | ||||
| 52 | // sendClose asks the client to close the window. | 75 | // sendClose asks the client to close the window. | ||
| 53 | func (w *window) sendClose() { | 76 | func (w *window) sendClose() { | ||
| 54 | - | if w.toplevel != nil { | 77 | + | switch { |
| 78 | + | case w.toplevel != nil: | |||
| 55 | w.toplevel.SendClose() | 79 | w.toplevel.SendClose() | ||
| 80 | + | case w.x11 != nil: | |||
| 81 | + | w.x11.server.x11Close(w) | |||
| 56 | } | 82 | } | ||
| 57 | } | 83 | } | ||
| 58 | 84 | ||||
| 59 | // toplevelAlive reports whether the window is a toplevel whose protocol | 85 | // toplevelAlive reports whether the window is a toplevel whose protocol | ||
| 60 | // object is alive. | 86 | // object is alive. | ||
| 61 | func (w *window) toplevelAlive() bool { | 87 | func (w *window) toplevelAlive() bool { | ||
| 88 | + | if w.x11 != nil { | |||
| 89 | + | return w.alive() | |||
| 90 | + | } | |||
| 62 | return w.toplevel != nil && w.toplevel.Resource.Alive() | 91 | return w.toplevel != nil && w.toplevel.Resource.Alive() | ||
| 63 | } | 92 | } | ||
| 64 | 93 | ||||
| 65 | // surfaceResource is the window's wl_surface, nil when it has none or it died. | 94 | // surfaceResource is the window's wl_surface, nil when it has none or it died. | ||
| 66 | func (w *window) surfaceResource() *wayland.Surface { | 95 | func (w *window) surfaceResource() *wayland.Surface { | ||
| 67 | - | if w.xdg != nil { | 96 | + | switch { |
| 97 | + | case w.xdg != nil: | |||
| 68 | return w.xdg.surfaceResource() | 98 | return w.xdg.surfaceResource() | ||
| 99 | + | case w.x11 != nil && w.alive(): | |||
| 100 | + | return w.x11.surface.wl | |||
| 69 | } | 101 | } | ||
| 70 | return nil | 102 | return nil | ||
| 71 | } | 103 | } | ||
| 104 | + | ||||
| 105 | + | // toplevelWindow is the toplevel window (xdg or X11) the surface is the role | |||
| 106 | + | // surface of, nil for other roles and popups' parents are not followed. | |||
| 107 | + | func (s *surface) toplevelWindow() *window { | |||
| 108 | + | if s.xdg != nil && s.xdg.window != nil { | |||
| 109 | + | return s.xdg.window | |||
| 110 | + | } | |||
| 111 | + | return s.x11win | |||
| 112 | + | } | |||
M internal/adapters/wayland/wire_client_test.go +3 -0
| @@ -55,6 +55,7 @@ import ( | |||
| 55 | 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | |
| 56 | 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | |
| 57 | 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 58 | + | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 58 | 59 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | |
| 59 | 60 | "github.com/bnema/purego-libwayland/server" | |
| 60 | 61 | "github.com/bnema/wlturbo" | |
| @@ -253,6 +254,8 @@ var wireSchemas = map[string]wireSchema{ | |||
| 253 | 254 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | |
| 254 | 255 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | |
| 255 | 256 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | |
| 257 | + | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | |
| 258 | + | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | |
| 256 | 259 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | |
| 257 | 260 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | |
| 258 | 261 | } | |
| @@ -55,6 +55,7 @@ import ( | |||||
| 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | ||
| 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | ||
| 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | ||
| 58 | + | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 58 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | 59 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | ||
| 59 | "github.com/bnema/purego-libwayland/server" | 60 | "github.com/bnema/purego-libwayland/server" | ||
| 60 | "github.com/bnema/wlturbo" | 61 | "github.com/bnema/wlturbo" | ||
| @@ -253,6 +254,8 @@ var wireSchemas = map[string]wireSchema{ | |||||
| 253 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | 254 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | ||
| 254 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | 255 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | ||
| 255 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | 256 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | ||
| 257 | + | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | |||
| 258 | + | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | |||
| 256 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | 259 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | ||
| 257 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | 260 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | ||
| 258 | } | 261 | } | ||
A internal/adapters/wayland/x11peer_test.go +298 -0
| @@ -0,0 +1,298 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "io" | |
| 6 | + | "os" | |
| 7 | + | "sync" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "golang.org/x/sys/unix" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // x11Peer is a scripted X server speaking the real wire protocol over a | |
| 15 | + | // socketpair: setup, extensions, atoms, properties, geometry. It records every | |
| 16 | + | // request. It is a protocol peer, not a double of a project interface. | |
| 17 | + | type x11Peer struct { | |
| 18 | + | t *testing.T | |
| 19 | + | f *os.File | |
| 20 | + | mu sync.Mutex | |
| 21 | + | ||
| 22 | + | seq uint16 | |
| 23 | + | reqs []x11Req | |
| 24 | + | atoms map[string]uint32 | |
| 25 | + | nextAtom uint32 | |
| 26 | + | props map[uint32]map[uint32][]byte // window -> atom -> typed data (type, format, data) | |
| 27 | + | propMeta map[uint32]map[uint32][2]uint32 | |
| 28 | + | done chan struct{} | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | type x11Req struct { | |
| 32 | + | major, minor uint8 | |
| 33 | + | raw []byte | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (r x11Req) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) } | |
| 37 | + | ||
| 38 | + | const ( | |
| 39 | + | x11PeerRoot = 0x100 | |
| 40 | + | x11Win = 0x500001 | |
| 41 | + | // Opcodes the tests look for. | |
| 42 | + | x11OpConfigureWindow = 12 | |
| 43 | + | x11OpSetInputFocus = 42 | |
| 44 | + | x11OpKillClient = 113 | |
| 45 | + | ) | |
| 46 | + | ||
| 47 | + | func startX11Peer(t *testing.T) (int, *x11Peer) { | |
| 48 | + | t.Helper() | |
| 49 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 50 | + | if err != nil { | |
| 51 | + | t.Fatal(err) | |
| 52 | + | } | |
| 53 | + | p := &x11Peer{ | |
| 54 | + | t: t, f: os.NewFile(uintptr(fds[1]), "x11peer"), atoms: map[string]uint32{}, nextAtom: 1000, | |
| 55 | + | props: map[uint32]map[uint32][]byte{}, propMeta: map[uint32]map[uint32][2]uint32{}, done: make(chan struct{}), | |
| 56 | + | } | |
| 57 | + | t.Cleanup(func() { p.f.Close(); <-p.done }) | |
| 58 | + | go p.serve() | |
| 59 | + | return fds[0], p | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func (p *x11Peer) setupReply() []byte { | |
| 63 | + | vendor := []byte("x11pr\x00\x00\x00") | |
| 64 | + | b := make([]byte, 40+len(vendor)+40) | |
| 65 | + | b[0], b[2] = 1, 11 | |
| 66 | + | binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4)) | |
| 67 | + | binary.LittleEndian.PutUint32(b[12:], 0x200000) | |
| 68 | + | binary.LittleEndian.PutUint32(b[16:], 0x1fffff) | |
| 69 | + | binary.LittleEndian.PutUint16(b[24:], 5) | |
| 70 | + | b[28] = 1 | |
| 71 | + | copy(b[40:], vendor) | |
| 72 | + | scr := b[40+len(vendor):] | |
| 73 | + | binary.LittleEndian.PutUint32(scr, x11PeerRoot) | |
| 74 | + | binary.LittleEndian.PutUint32(scr[32:], 0x22) | |
| 75 | + | scr[38] = 24 | |
| 76 | + | return b | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func (p *x11Peer) serve() { | |
| 80 | + | defer close(p.done) | |
| 81 | + | var hdr [12]byte | |
| 82 | + | if _, err := io.ReadFull(p.f, hdr[:]); err != nil { | |
| 83 | + | return | |
| 84 | + | } | |
| 85 | + | if _, err := p.f.Write(p.setupReply()); err != nil { | |
| 86 | + | return | |
| 87 | + | } | |
| 88 | + | for { | |
| 89 | + | var h [4]byte | |
| 90 | + | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | raw := h[:] | |
| 94 | + | n := int(binary.LittleEndian.Uint16(h[2:])) * 4 | |
| 95 | + | if n == 0 { | |
| 96 | + | var l [4]byte | |
| 97 | + | if _, err := io.ReadFull(p.f, l[:]); err != nil { | |
| 98 | + | return | |
| 99 | + | } | |
| 100 | + | raw = append(raw, l[:]...) | |
| 101 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |
| 102 | + | } | |
| 103 | + | rest := make([]byte, n-len(raw)) | |
| 104 | + | if _, err := io.ReadFull(p.f, rest); err != nil { | |
| 105 | + | return | |
| 106 | + | } | |
| 107 | + | raw = append(append([]byte(nil), raw...), rest...) | |
| 108 | + | p.mu.Lock() | |
| 109 | + | p.seq++ | |
| 110 | + | r := x11Req{h[0], h[1], raw} | |
| 111 | + | p.reqs = append(p.reqs, r) | |
| 112 | + | p.answer(r, raw[len(raw)-len(rest):]) | |
| 113 | + | p.mu.Unlock() | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | func (p *x11Peer) reply(data uint8, body []byte) { | |
| 118 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |
| 119 | + | b := make([]byte, 32+4*words) | |
| 120 | + | b[0], b[1] = 1, data | |
| 121 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |
| 122 | + | binary.LittleEndian.PutUint32(b[4:], uint32(words)) | |
| 123 | + | copy(b[8:], body) | |
| 124 | + | _, _ = p.f.Write(b) | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func x11le32(v ...uint32) []byte { | |
| 128 | + | b := make([]byte, 0, 4*len(v)) | |
| 129 | + | for _, x := range v { | |
| 130 | + | b = binary.LittleEndian.AppendUint32(b, x) | |
| 131 | + | } | |
| 132 | + | return b | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | var x11Exts = map[string]uint8{"BIG-REQUESTS": 133, "XC-MISC": 136, "Composite": 142} | |
| 136 | + | ||
| 137 | + | func (p *x11Peer) answer(r x11Req, body []byte) { | |
| 138 | + | switch { | |
| 139 | + | case r.major == 98: // QueryExtension | |
| 140 | + | major, ok := x11Exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |
| 141 | + | present := uint8(0) | |
| 142 | + | if ok { | |
| 143 | + | present = 1 | |
| 144 | + | } | |
| 145 | + | p.reply(0, []byte{present, major, 0, 0}) | |
| 146 | + | case r.major == 16: // InternAtom | |
| 147 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |
| 148 | + | a, ok := p.atoms[name] | |
| 149 | + | if !ok { | |
| 150 | + | p.nextAtom++ | |
| 151 | + | a = p.nextAtom | |
| 152 | + | p.atoms[name] = a | |
| 153 | + | } | |
| 154 | + | p.reply(0, x11le32(a)) | |
| 155 | + | case r.major == 43: // GetInputFocus (Sync) | |
| 156 | + | p.reply(1, x11le32(0)) | |
| 157 | + | case r.major == 15: // QueryTree | |
| 158 | + | b := make([]byte, 24) | |
| 159 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |
| 160 | + | p.reply(0, b) | |
| 161 | + | case r.major == 14: // GetGeometry | |
| 162 | + | b := make([]byte, 24) | |
| 163 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |
| 164 | + | binary.LittleEndian.PutUint16(b[8:], 300) | |
| 165 | + | binary.LittleEndian.PutUint16(b[10:], 200) | |
| 166 | + | p.reply(24, b) | |
| 167 | + | case r.major == 18: // ChangeProperty | |
| 168 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |
| 169 | + | format, n := body[12], int(binary.LittleEndian.Uint32(body[16:])) | |
| 170 | + | p.store(win, prop, binary.LittleEndian.Uint32(body[8:]), uint32(format), body[20:20+n*int(format)/8]) | |
| 171 | + | case r.major == 20: // GetProperty | |
| 172 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |
| 173 | + | data, ok := p.props[win][prop] | |
| 174 | + | b := make([]byte, 24+(len(data)+3)&^3) | |
| 175 | + | var format uint8 | |
| 176 | + | if ok { | |
| 177 | + | meta := p.propMeta[win][prop] | |
| 178 | + | format = uint8(meta[1]) | |
| 179 | + | binary.LittleEndian.PutUint32(b[0:], meta[0]) | |
| 180 | + | binary.LittleEndian.PutUint32(b[8:], uint32(len(data)/(int(format)/8))) | |
| 181 | + | copy(b[24:], data) | |
| 182 | + | } | |
| 183 | + | p.reply(format, b) | |
| 184 | + | case r.major >= 128: | |
| 185 | + | switch r.major { | |
| 186 | + | case 133: | |
| 187 | + | p.reply(0, x11le32(0x100000)) | |
| 188 | + | case 136: | |
| 189 | + | if r.minor == 1 { | |
| 190 | + | p.reply(0, x11le32(0, 1)) | |
| 191 | + | } | |
| 192 | + | case 142: | |
| 193 | + | if r.minor == 0 { | |
| 194 | + | p.reply(0, x11le32(0, 4)) | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | func (p *x11Peer) store(win, prop, typ, format uint32, data []byte) { | |
| 201 | + | if p.props[win] == nil { | |
| 202 | + | p.props[win], p.propMeta[win] = map[uint32][]byte{}, map[uint32][2]uint32{} | |
| 203 | + | } | |
| 204 | + | p.props[win][prop] = append([]byte(nil), data...) | |
| 205 | + | p.propMeta[win][prop] = [2]uint32{typ, format} | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // atom interns name the way the XWM would have. | |
| 209 | + | func (p *x11Peer) atom(name string) uint32 { | |
| 210 | + | p.mu.Lock() | |
| 211 | + | defer p.mu.Unlock() | |
| 212 | + | a, ok := p.atoms[name] | |
| 213 | + | if !ok { | |
| 214 | + | p.nextAtom++ | |
| 215 | + | a = p.nextAtom | |
| 216 | + | p.atoms[name] = a | |
| 217 | + | } | |
| 218 | + | return a | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // setProp scripts a window property (typ is an atom number, format 8 or 32). | |
| 222 | + | func (p *x11Peer) setProp(win, prop, typ, format uint32, data []byte) { | |
| 223 | + | p.mu.Lock() | |
| 224 | + | defer p.mu.Unlock() | |
| 225 | + | p.store(win, prop, typ, format, data) | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | func (p *x11Peer) event(code uint8, fill func(b *[32]byte)) { | |
| 229 | + | p.mu.Lock() | |
| 230 | + | defer p.mu.Unlock() | |
| 231 | + | var b [32]byte | |
| 232 | + | b[0] = code | |
| 233 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |
| 234 | + | fill(&b) | |
| 235 | + | _, _ = p.f.Write(b[:]) | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | func (p *x11Peer) createNotify(win uint32) { | |
| 239 | + | p.event(16, func(b *[32]byte) { | |
| 240 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 241 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 242 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |
| 243 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |
| 244 | + | }) | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | func (p *x11Peer) windowEvent(code uint8, win uint32) { | |
| 248 | + | p.event(code, func(b *[32]byte) { | |
| 249 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |
| 250 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 251 | + | }) | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | func (p *x11Peer) mapRequest(win uint32) { p.windowEvent(20, win) } | |
| 255 | + | func (p *x11Peer) unmapNotify(win uint32) { p.windowEvent(18, win) } | |
| 256 | + | func (p *x11Peer) destroyNotify(win uint32) { p.windowEvent(17, win) } | |
| 257 | + | ||
| 258 | + | func (p *x11Peer) propertyNotify(win, atom uint32) { | |
| 259 | + | p.event(28, func(b *[32]byte) { | |
| 260 | + | binary.LittleEndian.PutUint32(b[4:], win) | |
| 261 | + | binary.LittleEndian.PutUint32(b[8:], atom) | |
| 262 | + | }) | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | func (p *x11Peer) clientMessage(win uint32, typ string, data ...uint32) { | |
| 266 | + | t := p.atom(typ) | |
| 267 | + | p.event(33, func(b *[32]byte) { | |
| 268 | + | b[1] = 32 | |
| 269 | + | binary.LittleEndian.PutUint32(b[4:], win) | |
| 270 | + | binary.LittleEndian.PutUint32(b[8:], t) | |
| 271 | + | for i, d := range data { | |
| 272 | + | binary.LittleEndian.PutUint32(b[12+4*i:], d) | |
| 273 | + | } | |
| 274 | + | }) | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | func (p *x11Peer) requests() []x11Req { | |
| 278 | + | p.mu.Lock() | |
| 279 | + | defer p.mu.Unlock() | |
| 280 | + | return append([]x11Req(nil), p.reqs...) | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | // waitReq waits for a request after the first skip ones that satisfies match. | |
| 284 | + | func (p *x11Peer) waitReq(skip int, match func(x11Req) bool) (x11Req, int) { | |
| 285 | + | p.t.Helper() | |
| 286 | + | deadline := time.Now().Add(5 * time.Second) | |
| 287 | + | for time.Now().Before(deadline) { | |
| 288 | + | reqs := p.requests() | |
| 289 | + | for i := skip; i < len(reqs); i++ { | |
| 290 | + | if match(reqs[i]) { | |
| 291 | + | return reqs[i], i + 1 | |
| 292 | + | } | |
| 293 | + | } | |
| 294 | + | time.Sleep(time.Millisecond) | |
| 295 | + | } | |
| 296 | + | p.t.Fatal("x11 peer: expected request never arrived") | |
| 297 | + | return x11Req{}, 0 | |
| 298 | + | } | |
| @@ -0,0 +1,298 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "io" | |||
| 6 | + | "os" | |||
| 7 | + | "sync" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // x11Peer is a scripted X server speaking the real wire protocol over a | |||
| 15 | + | // socketpair: setup, extensions, atoms, properties, geometry. It records every | |||
| 16 | + | // request. It is a protocol peer, not a double of a project interface. | |||
| 17 | + | type x11Peer struct { | |||
| 18 | + | t *testing.T | |||
| 19 | + | f *os.File | |||
| 20 | + | mu sync.Mutex | |||
| 21 | + | ||||
| 22 | + | seq uint16 | |||
| 23 | + | reqs []x11Req | |||
| 24 | + | atoms map[string]uint32 | |||
| 25 | + | nextAtom uint32 | |||
| 26 | + | props map[uint32]map[uint32][]byte // window -> atom -> typed data (type, format, data) | |||
| 27 | + | propMeta map[uint32]map[uint32][2]uint32 | |||
| 28 | + | done chan struct{} | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | type x11Req struct { | |||
| 32 | + | major, minor uint8 | |||
| 33 | + | raw []byte | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func (r x11Req) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) } | |||
| 37 | + | ||||
| 38 | + | const ( | |||
| 39 | + | x11PeerRoot = 0x100 | |||
| 40 | + | x11Win = 0x500001 | |||
| 41 | + | // Opcodes the tests look for. | |||
| 42 | + | x11OpConfigureWindow = 12 | |||
| 43 | + | x11OpSetInputFocus = 42 | |||
| 44 | + | x11OpKillClient = 113 | |||
| 45 | + | ) | |||
| 46 | + | ||||
| 47 | + | func startX11Peer(t *testing.T) (int, *x11Peer) { | |||
| 48 | + | t.Helper() | |||
| 49 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 50 | + | if err != nil { | |||
| 51 | + | t.Fatal(err) | |||
| 52 | + | } | |||
| 53 | + | p := &x11Peer{ | |||
| 54 | + | t: t, f: os.NewFile(uintptr(fds[1]), "x11peer"), atoms: map[string]uint32{}, nextAtom: 1000, | |||
| 55 | + | props: map[uint32]map[uint32][]byte{}, propMeta: map[uint32]map[uint32][2]uint32{}, done: make(chan struct{}), | |||
| 56 | + | } | |||
| 57 | + | t.Cleanup(func() { p.f.Close(); <-p.done }) | |||
| 58 | + | go p.serve() | |||
| 59 | + | return fds[0], p | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func (p *x11Peer) setupReply() []byte { | |||
| 63 | + | vendor := []byte("x11pr\x00\x00\x00") | |||
| 64 | + | b := make([]byte, 40+len(vendor)+40) | |||
| 65 | + | b[0], b[2] = 1, 11 | |||
| 66 | + | binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4)) | |||
| 67 | + | binary.LittleEndian.PutUint32(b[12:], 0x200000) | |||
| 68 | + | binary.LittleEndian.PutUint32(b[16:], 0x1fffff) | |||
| 69 | + | binary.LittleEndian.PutUint16(b[24:], 5) | |||
| 70 | + | b[28] = 1 | |||
| 71 | + | copy(b[40:], vendor) | |||
| 72 | + | scr := b[40+len(vendor):] | |||
| 73 | + | binary.LittleEndian.PutUint32(scr, x11PeerRoot) | |||
| 74 | + | binary.LittleEndian.PutUint32(scr[32:], 0x22) | |||
| 75 | + | scr[38] = 24 | |||
| 76 | + | return b | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func (p *x11Peer) serve() { | |||
| 80 | + | defer close(p.done) | |||
| 81 | + | var hdr [12]byte | |||
| 82 | + | if _, err := io.ReadFull(p.f, hdr[:]); err != nil { | |||
| 83 | + | return | |||
| 84 | + | } | |||
| 85 | + | if _, err := p.f.Write(p.setupReply()); err != nil { | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | for { | |||
| 89 | + | var h [4]byte | |||
| 90 | + | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | raw := h[:] | |||
| 94 | + | n := int(binary.LittleEndian.Uint16(h[2:])) * 4 | |||
| 95 | + | if n == 0 { | |||
| 96 | + | var l [4]byte | |||
| 97 | + | if _, err := io.ReadFull(p.f, l[:]); err != nil { | |||
| 98 | + | return | |||
| 99 | + | } | |||
| 100 | + | raw = append(raw, l[:]...) | |||
| 101 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |||
| 102 | + | } | |||
| 103 | + | rest := make([]byte, n-len(raw)) | |||
| 104 | + | if _, err := io.ReadFull(p.f, rest); err != nil { | |||
| 105 | + | return | |||
| 106 | + | } | |||
| 107 | + | raw = append(append([]byte(nil), raw...), rest...) | |||
| 108 | + | p.mu.Lock() | |||
| 109 | + | p.seq++ | |||
| 110 | + | r := x11Req{h[0], h[1], raw} | |||
| 111 | + | p.reqs = append(p.reqs, r) | |||
| 112 | + | p.answer(r, raw[len(raw)-len(rest):]) | |||
| 113 | + | p.mu.Unlock() | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func (p *x11Peer) reply(data uint8, body []byte) { | |||
| 118 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |||
| 119 | + | b := make([]byte, 32+4*words) | |||
| 120 | + | b[0], b[1] = 1, data | |||
| 121 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 122 | + | binary.LittleEndian.PutUint32(b[4:], uint32(words)) | |||
| 123 | + | copy(b[8:], body) | |||
| 124 | + | _, _ = p.f.Write(b) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func x11le32(v ...uint32) []byte { | |||
| 128 | + | b := make([]byte, 0, 4*len(v)) | |||
| 129 | + | for _, x := range v { | |||
| 130 | + | b = binary.LittleEndian.AppendUint32(b, x) | |||
| 131 | + | } | |||
| 132 | + | return b | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | var x11Exts = map[string]uint8{"BIG-REQUESTS": 133, "XC-MISC": 136, "Composite": 142} | |||
| 136 | + | ||||
| 137 | + | func (p *x11Peer) answer(r x11Req, body []byte) { | |||
| 138 | + | switch { | |||
| 139 | + | case r.major == 98: // QueryExtension | |||
| 140 | + | major, ok := x11Exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |||
| 141 | + | present := uint8(0) | |||
| 142 | + | if ok { | |||
| 143 | + | present = 1 | |||
| 144 | + | } | |||
| 145 | + | p.reply(0, []byte{present, major, 0, 0}) | |||
| 146 | + | case r.major == 16: // InternAtom | |||
| 147 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |||
| 148 | + | a, ok := p.atoms[name] | |||
| 149 | + | if !ok { | |||
| 150 | + | p.nextAtom++ | |||
| 151 | + | a = p.nextAtom | |||
| 152 | + | p.atoms[name] = a | |||
| 153 | + | } | |||
| 154 | + | p.reply(0, x11le32(a)) | |||
| 155 | + | case r.major == 43: // GetInputFocus (Sync) | |||
| 156 | + | p.reply(1, x11le32(0)) | |||
| 157 | + | case r.major == 15: // QueryTree | |||
| 158 | + | b := make([]byte, 24) | |||
| 159 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |||
| 160 | + | p.reply(0, b) | |||
| 161 | + | case r.major == 14: // GetGeometry | |||
| 162 | + | b := make([]byte, 24) | |||
| 163 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |||
| 164 | + | binary.LittleEndian.PutUint16(b[8:], 300) | |||
| 165 | + | binary.LittleEndian.PutUint16(b[10:], 200) | |||
| 166 | + | p.reply(24, b) | |||
| 167 | + | case r.major == 18: // ChangeProperty | |||
| 168 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 169 | + | format, n := body[12], int(binary.LittleEndian.Uint32(body[16:])) | |||
| 170 | + | p.store(win, prop, binary.LittleEndian.Uint32(body[8:]), uint32(format), body[20:20+n*int(format)/8]) | |||
| 171 | + | case r.major == 20: // GetProperty | |||
| 172 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 173 | + | data, ok := p.props[win][prop] | |||
| 174 | + | b := make([]byte, 24+(len(data)+3)&^3) | |||
| 175 | + | var format uint8 | |||
| 176 | + | if ok { | |||
| 177 | + | meta := p.propMeta[win][prop] | |||
| 178 | + | format = uint8(meta[1]) | |||
| 179 | + | binary.LittleEndian.PutUint32(b[0:], meta[0]) | |||
| 180 | + | binary.LittleEndian.PutUint32(b[8:], uint32(len(data)/(int(format)/8))) | |||
| 181 | + | copy(b[24:], data) | |||
| 182 | + | } | |||
| 183 | + | p.reply(format, b) | |||
| 184 | + | case r.major >= 128: | |||
| 185 | + | switch r.major { | |||
| 186 | + | case 133: | |||
| 187 | + | p.reply(0, x11le32(0x100000)) | |||
| 188 | + | case 136: | |||
| 189 | + | if r.minor == 1 { | |||
| 190 | + | p.reply(0, x11le32(0, 1)) | |||
| 191 | + | } | |||
| 192 | + | case 142: | |||
| 193 | + | if r.minor == 0 { | |||
| 194 | + | p.reply(0, x11le32(0, 4)) | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | func (p *x11Peer) store(win, prop, typ, format uint32, data []byte) { | |||
| 201 | + | if p.props[win] == nil { | |||
| 202 | + | p.props[win], p.propMeta[win] = map[uint32][]byte{}, map[uint32][2]uint32{} | |||
| 203 | + | } | |||
| 204 | + | p.props[win][prop] = append([]byte(nil), data...) | |||
| 205 | + | p.propMeta[win][prop] = [2]uint32{typ, format} | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // atom interns name the way the XWM would have. | |||
| 209 | + | func (p *x11Peer) atom(name string) uint32 { | |||
| 210 | + | p.mu.Lock() | |||
| 211 | + | defer p.mu.Unlock() | |||
| 212 | + | a, ok := p.atoms[name] | |||
| 213 | + | if !ok { | |||
| 214 | + | p.nextAtom++ | |||
| 215 | + | a = p.nextAtom | |||
| 216 | + | p.atoms[name] = a | |||
| 217 | + | } | |||
| 218 | + | return a | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // setProp scripts a window property (typ is an atom number, format 8 or 32). | |||
| 222 | + | func (p *x11Peer) setProp(win, prop, typ, format uint32, data []byte) { | |||
| 223 | + | p.mu.Lock() | |||
| 224 | + | defer p.mu.Unlock() | |||
| 225 | + | p.store(win, prop, typ, format, data) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | func (p *x11Peer) event(code uint8, fill func(b *[32]byte)) { | |||
| 229 | + | p.mu.Lock() | |||
| 230 | + | defer p.mu.Unlock() | |||
| 231 | + | var b [32]byte | |||
| 232 | + | b[0] = code | |||
| 233 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 234 | + | fill(&b) | |||
| 235 | + | _, _ = p.f.Write(b[:]) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func (p *x11Peer) createNotify(win uint32) { | |||
| 239 | + | p.event(16, func(b *[32]byte) { | |||
| 240 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 241 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 242 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |||
| 243 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |||
| 244 | + | }) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | func (p *x11Peer) windowEvent(code uint8, win uint32) { | |||
| 248 | + | p.event(code, func(b *[32]byte) { | |||
| 249 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 250 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 251 | + | }) | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func (p *x11Peer) mapRequest(win uint32) { p.windowEvent(20, win) } | |||
| 255 | + | func (p *x11Peer) unmapNotify(win uint32) { p.windowEvent(18, win) } | |||
| 256 | + | func (p *x11Peer) destroyNotify(win uint32) { p.windowEvent(17, win) } | |||
| 257 | + | ||||
| 258 | + | func (p *x11Peer) propertyNotify(win, atom uint32) { | |||
| 259 | + | p.event(28, func(b *[32]byte) { | |||
| 260 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 261 | + | binary.LittleEndian.PutUint32(b[8:], atom) | |||
| 262 | + | }) | |||
| 263 | + | } | |||
| 264 | + | ||||
| 265 | + | func (p *x11Peer) clientMessage(win uint32, typ string, data ...uint32) { | |||
| 266 | + | t := p.atom(typ) | |||
| 267 | + | p.event(33, func(b *[32]byte) { | |||
| 268 | + | b[1] = 32 | |||
| 269 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 270 | + | binary.LittleEndian.PutUint32(b[8:], t) | |||
| 271 | + | for i, d := range data { | |||
| 272 | + | binary.LittleEndian.PutUint32(b[12+4*i:], d) | |||
| 273 | + | } | |||
| 274 | + | }) | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | func (p *x11Peer) requests() []x11Req { | |||
| 278 | + | p.mu.Lock() | |||
| 279 | + | defer p.mu.Unlock() | |||
| 280 | + | return append([]x11Req(nil), p.reqs...) | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | // waitReq waits for a request after the first skip ones that satisfies match. | |||
| 284 | + | func (p *x11Peer) waitReq(skip int, match func(x11Req) bool) (x11Req, int) { | |||
| 285 | + | p.t.Helper() | |||
| 286 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 287 | + | for time.Now().Before(deadline) { | |||
| 288 | + | reqs := p.requests() | |||
| 289 | + | for i := skip; i < len(reqs); i++ { | |||
| 290 | + | if match(reqs[i]) { | |||
| 291 | + | return reqs[i], i + 1 | |||
| 292 | + | } | |||
| 293 | + | } | |||
| 294 | + | time.Sleep(time.Millisecond) | |||
| 295 | + | } | |||
| 296 | + | p.t.Fatal("x11 peer: expected request never arrived") | |||
| 297 | + | return x11Req{}, 0 | |||
| 298 | + | } | |||
A internal/adapters/wayland/x11window.go +298 -0
| @@ -0,0 +1,298 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 5 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 6 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // x11Window is the X11 role of a window: the XWM slot, the window's X id and | |
| 12 | + | // the wl_surface Xwayland paired with it. Display goroutine only. | |
| 13 | + | type x11Window struct { | |
| 14 | + | server *Server | |
| 15 | + | slot xwm.Slot | |
| 16 | + | xid uint32 | |
| 17 | + | surface *surface | |
| 18 | + | info xwm.WindowInfo | |
| 19 | + | // ready is set once the XWM read the window's properties (Ready). | |
| 20 | + | ready bool | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // xwaylandRestricted are the globals only the Xwayland client may see. | |
| 24 | + | func xwaylandRestricted(iface string) bool { | |
| 25 | + | return iface == "xwayland_shell_v1" || iface == "zwp_xwayland_keyboard_grab_manager_v1" | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // globalVisible is the display's global filter: the Xwayland-only globals | |
| 29 | + | // are visible to, and bindable by, the Xwayland client alone. | |
| 30 | + | func (s *Server) globalVisible(c server.Client, g *server.Global) bool { | |
| 31 | + | if xwaylandRestricted(g.Interface()) { | |
| 32 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |
| 33 | + | } | |
| 34 | + | return true | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func registerXwaylandShell(d *server.Display, s *Server) error { | |
| 38 | + | return xwaylandshell.NewXwaylandShellV1Global(d, 1, func(c server.Client, v, id uint32) { | |
| 39 | + | _, _ = xwaylandshell.NewXwaylandShellV1(c, int32(v), id, xwaylandShell{s}) | |
| 40 | + | }) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | type xwaylandShell struct{ s *Server } | |
| 44 | + | ||
| 45 | + | func (xwaylandShell) Destroy(*xwaylandshell.XwaylandShellV1) {} | |
| 46 | + | ||
| 47 | + | func (h xwaylandShell) GetXwaylandSurface(r *xwaylandshell.XwaylandShellV1, id uint32, w *wayland.Surface) { | |
| 48 | + | surf := h.s.surfaceOf(w) | |
| 49 | + | if surf == nil { | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | if surf.kind != roleNone { | |
| 53 | + | r.PostError(uint32(xwaylandshell.XwaylandShellV1ErrorRole), "surface already has a role") | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | if _, err := xwaylandshell.NewXwaylandSurfaceV1(r.Client(), r.Version(), id, xwaylandSurface{h.s, surf}); err != nil { | |
| 57 | + | return | |
| 58 | + | } | |
| 59 | + | surf.kind = roleX11 | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | type xwaylandSurface struct { | |
| 63 | + | s *Server | |
| 64 | + | surf *surface | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func (x xwaylandSurface) SetSerial(r *xwaylandshell.XwaylandSurfaceV1, lo, hi uint32) { | |
| 68 | + | serial := uint64(hi)<<32 | uint64(lo) | |
| 69 | + | switch { | |
| 70 | + | case x.surf.x11Serial != 0: | |
| 71 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated), "serial already set") | |
| 72 | + | case serial == 0: | |
| 73 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial), "serial is zero") | |
| 74 | + | default: | |
| 75 | + | x.surf.x11Serial = serial | |
| 76 | + | if x.s.xwm != nil { | |
| 77 | + | x.s.xwm.SetSerial(x.surf.identity, serial) | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | func (xwaylandSurface) Destroy(*xwaylandshell.XwaylandSurfaceV1) {} | |
| 83 | + | ||
| 84 | + | // installXWM makes x the server's window manager. Display goroutine only. | |
| 85 | + | func (s *Server) installXWM(x *xwm.XWM) { | |
| 86 | + | s.teardownXWM() | |
| 87 | + | s.xwm = x | |
| 88 | + | if err := x.Attach(s.display, hostImpl{s}); err != nil { | |
| 89 | + | s.log.Warn().Err(err).Msg("X11 window management disabled") | |
| 90 | + | s.xwm = nil | |
| 91 | + | x.CloseConn() | |
| 92 | + | return | |
| 93 | + | } | |
| 94 | + | // Serials sent before the XWM was attached pair now. | |
| 95 | + | for _, surf := range s.surfaces { | |
| 96 | + | if surf.kind == roleX11 && surf.x11Serial != 0 && surf.x11win == nil { | |
| 97 | + | x.SetSerial(surf.identity, surf.x11Serial) | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // teardownXWM closes the window manager, dropping its windows. Idempotent. | |
| 103 | + | func (s *Server) teardownXWM() { | |
| 104 | + | if x := s.xwm; x != nil { | |
| 105 | + | s.xwm = nil | |
| 106 | + | x.Close() | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // x11Configure forwards the latest core configure of an X11 window. | |
| 111 | + | func (s *Server) x11Configure(w *window) { | |
| 112 | + | if s.xwm != nil { | |
| 113 | + | s.xwm.Configure(w.x11.slot, w.last) | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // x11Close asks an X11 window to close. | |
| 118 | + | func (s *Server) x11Close(w *window) { | |
| 119 | + | if s.xwm != nil { | |
| 120 | + | s.xwm.CloseWindow(w.x11.slot) | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // x11Place forwards core's position of an X11 window's client rect. | |
| 125 | + | func (s *Server) x11Place(c ports.PlaceWindow) { | |
| 126 | + | if w := s.windows[c.ID]; w != nil && w.x11 != nil && s.xwm != nil { | |
| 127 | + | s.xwm.Place(w.x11.slot, c.X, c.Y) | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // x11Focus tells the XWM which X window holds the keyboard focus, none when a | |
| 132 | + | // native window does. | |
| 133 | + | func (s *Server) x11Focus() { | |
| 134 | + | if s.xwm == nil { | |
| 135 | + | return | |
| 136 | + | } | |
| 137 | + | var slot xwm.Slot | |
| 138 | + | if w := s.windows[s.seat.focused]; w != nil && w.x11 != nil { | |
| 139 | + | slot = w.x11.slot | |
| 140 | + | } | |
| 141 | + | s.xwm.Focus(slot) | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // hostImpl is the server as the XWM sees it. | |
| 145 | + | type hostImpl struct{ s *Server } | |
| 146 | + | ||
| 147 | + | func (h hostImpl) window(slot xwm.Slot) *window { return h.s.x11Slots[slot] } | |
| 148 | + | ||
| 149 | + | func (h hostImpl) Created(slot xwm.Slot, xid uint32) ports.WindowID { | |
| 150 | + | s := h.s | |
| 151 | + | w := &window{id: s.nextWindow} | |
| 152 | + | w.x11 = &x11Window{server: s, slot: slot, xid: xid} | |
| 153 | + | s.nextWindow++ | |
| 154 | + | s.windows[w.id] = w | |
| 155 | + | s.x11Slots[slot] = w | |
| 156 | + | return w.id | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | func (h hostImpl) Ready(slot xwm.Slot, info xwm.WindowInfo) { | |
| 160 | + | w := h.window(slot) | |
| 161 | + | if w == nil { | |
| 162 | + | return | |
| 163 | + | } | |
| 164 | + | w.x11.info, w.x11.ready = info, true | |
| 165 | + | w.title, w.appID, w.modal = info.Title, info.AppID, info.Modal | |
| 166 | + | w.parent = h.window(info.Parent) | |
| 167 | + | // A surface associated and committed earlier maps now. | |
| 168 | + | w.x11.tryMap() | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func (h hostImpl) Associate(slot xwm.Slot, surfaceID uint64) { | |
| 172 | + | w := h.window(slot) | |
| 173 | + | surf := h.s.surfaceByIdentity[surfaceID] | |
| 174 | + | if w == nil || surf == nil || surf.kind != roleX11 { | |
| 175 | + | return | |
| 176 | + | } | |
| 177 | + | x := w.x11 | |
| 178 | + | x.drop() | |
| 179 | + | x.surface, surf.x11win = surf, w | |
| 180 | + | surf.role = func(buffer bool) { | |
| 181 | + | switch { | |
| 182 | + | case surf.destroyed: | |
| 183 | + | w.unmap() | |
| 184 | + | if x.surface == surf { | |
| 185 | + | x.surface = nil | |
| 186 | + | } | |
| 187 | + | surf.x11win = nil | |
| 188 | + | case buffer: | |
| 189 | + | x.tryMap() | |
| 190 | + | case w.mapped: | |
| 191 | + | w.unmap() | |
| 192 | + | } | |
| 193 | + | } | |
| 194 | + | // The buffer may have been committed before the pairing. | |
| 195 | + | x.tryMap() | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) { | |
| 199 | + | w := h.window(slot) | |
| 200 | + | if w == nil { | |
| 201 | + | return | |
| 202 | + | } | |
| 203 | + | w.unmap() | |
| 204 | + | w.x11.drop() | |
| 205 | + | w.x11.ready = false | |
| 206 | + | if destroyed { | |
| 207 | + | delete(h.s.windows, w.id) | |
| 208 | + | delete(h.s.x11Slots, slot) | |
| 209 | + | h.s.foreignToplevelGone(w) | |
| 210 | + | } | |
| 211 | + | } | |
| 212 | + | ||
| 213 | + | func (h hostImpl) Title(slot xwm.Slot, title string) { | |
| 214 | + | if w := h.window(slot); w != nil && w.title != title { | |
| 215 | + | w.title = title | |
| 216 | + | h.s.toplevelChanged(w) | |
| 217 | + | if w.mapped { | |
| 218 | + | h.s.castInventoryChanged() | |
| 219 | + | } | |
| 220 | + | } | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | func (h hostImpl) AppID(slot xwm.Slot, appID string) { | |
| 224 | + | w := h.window(slot) | |
| 225 | + | if w == nil || w.appID == appID { | |
| 226 | + | return | |
| 227 | + | } | |
| 228 | + | w.appID = appID | |
| 229 | + | if w.mapped { | |
| 230 | + | h.s.emit(ports.WindowAppID{ID: w.id, AppID: appID}) | |
| 231 | + | h.s.toplevelChanged(w) | |
| 232 | + | h.s.castInventoryChanged() | |
| 233 | + | } | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | func (h hostImpl) Parent(slot xwm.Slot, parent xwm.Slot) { | |
| 237 | + | if w := h.window(slot); w != nil { | |
| 238 | + | w.setParent(h.window(parent)) | |
| 239 | + | } | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | func (h hostImpl) Fullscreen(slot xwm.Slot, on bool) { | |
| 243 | + | w := h.window(slot) | |
| 244 | + | if w == nil { | |
| 245 | + | return | |
| 246 | + | } | |
| 247 | + | w.x11.info.Fullscreen = on | |
| 248 | + | if w.mapped { | |
| 249 | + | h.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: on}) | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | // drop unlinks the window from its surface. | |
| 254 | + | func (x *x11Window) drop() { | |
| 255 | + | if surf := x.surface; surf != nil { | |
| 256 | + | surf.x11win, surf.role = nil, nil | |
| 257 | + | x.surface = nil | |
| 258 | + | } | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | // tryMap maps the window once the XWM knows it and its surface shows a buffer. | |
| 262 | + | func (x *x11Window) tryMap() { | |
| 263 | + | surf := x.surface | |
| 264 | + | if surf == nil { | |
| 265 | + | return | |
| 266 | + | } | |
| 267 | + | w := surf.x11win | |
| 268 | + | if w == nil || w.mapped || !x.ready || surf.destroyed || surf.current == nil { | |
| 269 | + | return | |
| 270 | + | } | |
| 271 | + | s := x.server | |
| 272 | + | w.mapped = true | |
| 273 | + | w.gen++ | |
| 274 | + | w.floating = x.info.Floating | |
| 275 | + | w.floatW, w.floatH = 0, 0 | |
| 276 | + | if w.floating { | |
| 277 | + | w.floatW, w.floatH = x.info.Width, x.info.Height | |
| 278 | + | } | |
| 279 | + | var parent ports.WindowID | |
| 280 | + | if w.parent != nil && w.parent.mapped { | |
| 281 | + | parent = w.parent.id | |
| 282 | + | } | |
| 283 | + | s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1}) | |
| 284 | + | if x.info.Fullscreen { | |
| 285 | + | s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true}) | |
| 286 | + | } | |
| 287 | + | s.syncInhibitors() | |
| 288 | + | s.toplevelChanged(w) | |
| 289 | + | s.castInventoryChanged() | |
| 290 | + | s.log.Info().Uint64("id", uint64(w.id)).Uint32("xid", x.xid).Str("app_id", w.appID).Bool("floating", w.floating).Msg("x11 window mapped") | |
| 291 | + | // Called from a commit, the content follows right after; called from the | |
| 292 | + | // pairing, the surface already shows a buffer and is published now. | |
| 293 | + | if surf.has { | |
| 294 | + | surf.resetInputEmission() | |
| 295 | + | surf.redraw() | |
| 296 | + | surf.emitInput() | |
| 297 | + | } | |
| 298 | + | } | |
| @@ -0,0 +1,298 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 5 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // x11Window is the X11 role of a window: the XWM slot, the window's X id and | |||
| 12 | + | // the wl_surface Xwayland paired with it. Display goroutine only. | |||
| 13 | + | type x11Window struct { | |||
| 14 | + | server *Server | |||
| 15 | + | slot xwm.Slot | |||
| 16 | + | xid uint32 | |||
| 17 | + | surface *surface | |||
| 18 | + | info xwm.WindowInfo | |||
| 19 | + | // ready is set once the XWM read the window's properties (Ready). | |||
| 20 | + | ready bool | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // xwaylandRestricted are the globals only the Xwayland client may see. | |||
| 24 | + | func xwaylandRestricted(iface string) bool { | |||
| 25 | + | return iface == "xwayland_shell_v1" || iface == "zwp_xwayland_keyboard_grab_manager_v1" | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // globalVisible is the display's global filter: the Xwayland-only globals | |||
| 29 | + | // are visible to, and bindable by, the Xwayland client alone. | |||
| 30 | + | func (s *Server) globalVisible(c server.Client, g *server.Global) bool { | |||
| 31 | + | if xwaylandRestricted(g.Interface()) { | |||
| 32 | + | return s.xwayland.client != (server.Client{}) && c == s.xwayland.client | |||
| 33 | + | } | |||
| 34 | + | return true | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func registerXwaylandShell(d *server.Display, s *Server) error { | |||
| 38 | + | return xwaylandshell.NewXwaylandShellV1Global(d, 1, func(c server.Client, v, id uint32) { | |||
| 39 | + | _, _ = xwaylandshell.NewXwaylandShellV1(c, int32(v), id, xwaylandShell{s}) | |||
| 40 | + | }) | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | type xwaylandShell struct{ s *Server } | |||
| 44 | + | ||||
| 45 | + | func (xwaylandShell) Destroy(*xwaylandshell.XwaylandShellV1) {} | |||
| 46 | + | ||||
| 47 | + | func (h xwaylandShell) GetXwaylandSurface(r *xwaylandshell.XwaylandShellV1, id uint32, w *wayland.Surface) { | |||
| 48 | + | surf := h.s.surfaceOf(w) | |||
| 49 | + | if surf == nil { | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | if surf.kind != roleNone { | |||
| 53 | + | r.PostError(uint32(xwaylandshell.XwaylandShellV1ErrorRole), "surface already has a role") | |||
| 54 | + | return | |||
| 55 | + | } | |||
| 56 | + | if _, err := xwaylandshell.NewXwaylandSurfaceV1(r.Client(), r.Version(), id, xwaylandSurface{h.s, surf}); err != nil { | |||
| 57 | + | return | |||
| 58 | + | } | |||
| 59 | + | surf.kind = roleX11 | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | type xwaylandSurface struct { | |||
| 63 | + | s *Server | |||
| 64 | + | surf *surface | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func (x xwaylandSurface) SetSerial(r *xwaylandshell.XwaylandSurfaceV1, lo, hi uint32) { | |||
| 68 | + | serial := uint64(hi)<<32 | uint64(lo) | |||
| 69 | + | switch { | |||
| 70 | + | case x.surf.x11Serial != 0: | |||
| 71 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated), "serial already set") | |||
| 72 | + | case serial == 0: | |||
| 73 | + | r.PostError(uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial), "serial is zero") | |||
| 74 | + | default: | |||
| 75 | + | x.surf.x11Serial = serial | |||
| 76 | + | if x.s.xwm != nil { | |||
| 77 | + | x.s.xwm.SetSerial(x.surf.identity, serial) | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | func (xwaylandSurface) Destroy(*xwaylandshell.XwaylandSurfaceV1) {} | |||
| 83 | + | ||||
| 84 | + | // installXWM makes x the server's window manager. Display goroutine only. | |||
| 85 | + | func (s *Server) installXWM(x *xwm.XWM) { | |||
| 86 | + | s.teardownXWM() | |||
| 87 | + | s.xwm = x | |||
| 88 | + | if err := x.Attach(s.display, hostImpl{s}); err != nil { | |||
| 89 | + | s.log.Warn().Err(err).Msg("X11 window management disabled") | |||
| 90 | + | s.xwm = nil | |||
| 91 | + | x.CloseConn() | |||
| 92 | + | return | |||
| 93 | + | } | |||
| 94 | + | // Serials sent before the XWM was attached pair now. | |||
| 95 | + | for _, surf := range s.surfaces { | |||
| 96 | + | if surf.kind == roleX11 && surf.x11Serial != 0 && surf.x11win == nil { | |||
| 97 | + | x.SetSerial(surf.identity, surf.x11Serial) | |||
| 98 | + | } | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // teardownXWM closes the window manager, dropping its windows. Idempotent. | |||
| 103 | + | func (s *Server) teardownXWM() { | |||
| 104 | + | if x := s.xwm; x != nil { | |||
| 105 | + | s.xwm = nil | |||
| 106 | + | x.Close() | |||
| 107 | + | } | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // x11Configure forwards the latest core configure of an X11 window. | |||
| 111 | + | func (s *Server) x11Configure(w *window) { | |||
| 112 | + | if s.xwm != nil { | |||
| 113 | + | s.xwm.Configure(w.x11.slot, w.last) | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | // x11Close asks an X11 window to close. | |||
| 118 | + | func (s *Server) x11Close(w *window) { | |||
| 119 | + | if s.xwm != nil { | |||
| 120 | + | s.xwm.CloseWindow(w.x11.slot) | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // x11Place forwards core's position of an X11 window's client rect. | |||
| 125 | + | func (s *Server) x11Place(c ports.PlaceWindow) { | |||
| 126 | + | if w := s.windows[c.ID]; w != nil && w.x11 != nil && s.xwm != nil { | |||
| 127 | + | s.xwm.Place(w.x11.slot, c.X, c.Y) | |||
| 128 | + | } | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // x11Focus tells the XWM which X window holds the keyboard focus, none when a | |||
| 132 | + | // native window does. | |||
| 133 | + | func (s *Server) x11Focus() { | |||
| 134 | + | if s.xwm == nil { | |||
| 135 | + | return | |||
| 136 | + | } | |||
| 137 | + | var slot xwm.Slot | |||
| 138 | + | if w := s.windows[s.seat.focused]; w != nil && w.x11 != nil { | |||
| 139 | + | slot = w.x11.slot | |||
| 140 | + | } | |||
| 141 | + | s.xwm.Focus(slot) | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | // hostImpl is the server as the XWM sees it. | |||
| 145 | + | type hostImpl struct{ s *Server } | |||
| 146 | + | ||||
| 147 | + | func (h hostImpl) window(slot xwm.Slot) *window { return h.s.x11Slots[slot] } | |||
| 148 | + | ||||
| 149 | + | func (h hostImpl) Created(slot xwm.Slot, xid uint32) ports.WindowID { | |||
| 150 | + | s := h.s | |||
| 151 | + | w := &window{id: s.nextWindow} | |||
| 152 | + | w.x11 = &x11Window{server: s, slot: slot, xid: xid} | |||
| 153 | + | s.nextWindow++ | |||
| 154 | + | s.windows[w.id] = w | |||
| 155 | + | s.x11Slots[slot] = w | |||
| 156 | + | return w.id | |||
| 157 | + | } | |||
| 158 | + | ||||
| 159 | + | func (h hostImpl) Ready(slot xwm.Slot, info xwm.WindowInfo) { | |||
| 160 | + | w := h.window(slot) | |||
| 161 | + | if w == nil { | |||
| 162 | + | return | |||
| 163 | + | } | |||
| 164 | + | w.x11.info, w.x11.ready = info, true | |||
| 165 | + | w.title, w.appID, w.modal = info.Title, info.AppID, info.Modal | |||
| 166 | + | w.parent = h.window(info.Parent) | |||
| 167 | + | // A surface associated and committed earlier maps now. | |||
| 168 | + | w.x11.tryMap() | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func (h hostImpl) Associate(slot xwm.Slot, surfaceID uint64) { | |||
| 172 | + | w := h.window(slot) | |||
| 173 | + | surf := h.s.surfaceByIdentity[surfaceID] | |||
| 174 | + | if w == nil || surf == nil || surf.kind != roleX11 { | |||
| 175 | + | return | |||
| 176 | + | } | |||
| 177 | + | x := w.x11 | |||
| 178 | + | x.drop() | |||
| 179 | + | x.surface, surf.x11win = surf, w | |||
| 180 | + | surf.role = func(buffer bool) { | |||
| 181 | + | switch { | |||
| 182 | + | case surf.destroyed: | |||
| 183 | + | w.unmap() | |||
| 184 | + | if x.surface == surf { | |||
| 185 | + | x.surface = nil | |||
| 186 | + | } | |||
| 187 | + | surf.x11win = nil | |||
| 188 | + | case buffer: | |||
| 189 | + | x.tryMap() | |||
| 190 | + | case w.mapped: | |||
| 191 | + | w.unmap() | |||
| 192 | + | } | |||
| 193 | + | } | |||
| 194 | + | // The buffer may have been committed before the pairing. | |||
| 195 | + | x.tryMap() | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) { | |||
| 199 | + | w := h.window(slot) | |||
| 200 | + | if w == nil { | |||
| 201 | + | return | |||
| 202 | + | } | |||
| 203 | + | w.unmap() | |||
| 204 | + | w.x11.drop() | |||
| 205 | + | w.x11.ready = false | |||
| 206 | + | if destroyed { | |||
| 207 | + | delete(h.s.windows, w.id) | |||
| 208 | + | delete(h.s.x11Slots, slot) | |||
| 209 | + | h.s.foreignToplevelGone(w) | |||
| 210 | + | } | |||
| 211 | + | } | |||
| 212 | + | ||||
| 213 | + | func (h hostImpl) Title(slot xwm.Slot, title string) { | |||
| 214 | + | if w := h.window(slot); w != nil && w.title != title { | |||
| 215 | + | w.title = title | |||
| 216 | + | h.s.toplevelChanged(w) | |||
| 217 | + | if w.mapped { | |||
| 218 | + | h.s.castInventoryChanged() | |||
| 219 | + | } | |||
| 220 | + | } | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | func (h hostImpl) AppID(slot xwm.Slot, appID string) { | |||
| 224 | + | w := h.window(slot) | |||
| 225 | + | if w == nil || w.appID == appID { | |||
| 226 | + | return | |||
| 227 | + | } | |||
| 228 | + | w.appID = appID | |||
| 229 | + | if w.mapped { | |||
| 230 | + | h.s.emit(ports.WindowAppID{ID: w.id, AppID: appID}) | |||
| 231 | + | h.s.toplevelChanged(w) | |||
| 232 | + | h.s.castInventoryChanged() | |||
| 233 | + | } | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | func (h hostImpl) Parent(slot xwm.Slot, parent xwm.Slot) { | |||
| 237 | + | if w := h.window(slot); w != nil { | |||
| 238 | + | w.setParent(h.window(parent)) | |||
| 239 | + | } | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | func (h hostImpl) Fullscreen(slot xwm.Slot, on bool) { | |||
| 243 | + | w := h.window(slot) | |||
| 244 | + | if w == nil { | |||
| 245 | + | return | |||
| 246 | + | } | |||
| 247 | + | w.x11.info.Fullscreen = on | |||
| 248 | + | if w.mapped { | |||
| 249 | + | h.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: on}) | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | ||||
| 253 | + | // drop unlinks the window from its surface. | |||
| 254 | + | func (x *x11Window) drop() { | |||
| 255 | + | if surf := x.surface; surf != nil { | |||
| 256 | + | surf.x11win, surf.role = nil, nil | |||
| 257 | + | x.surface = nil | |||
| 258 | + | } | |||
| 259 | + | } | |||
| 260 | + | ||||
| 261 | + | // tryMap maps the window once the XWM knows it and its surface shows a buffer. | |||
| 262 | + | func (x *x11Window) tryMap() { | |||
| 263 | + | surf := x.surface | |||
| 264 | + | if surf == nil { | |||
| 265 | + | return | |||
| 266 | + | } | |||
| 267 | + | w := surf.x11win | |||
| 268 | + | if w == nil || w.mapped || !x.ready || surf.destroyed || surf.current == nil { | |||
| 269 | + | return | |||
| 270 | + | } | |||
| 271 | + | s := x.server | |||
| 272 | + | w.mapped = true | |||
| 273 | + | w.gen++ | |||
| 274 | + | w.floating = x.info.Floating | |||
| 275 | + | w.floatW, w.floatH = 0, 0 | |||
| 276 | + | if w.floating { | |||
| 277 | + | w.floatW, w.floatH = x.info.Width, x.info.Height | |||
| 278 | + | } | |||
| 279 | + | var parent ports.WindowID | |||
| 280 | + | if w.parent != nil && w.parent.mapped { | |||
| 281 | + | parent = w.parent.id | |||
| 282 | + | } | |||
| 283 | + | s.emit(ports.WindowMapped{ID: w.id, AppID: w.appID, PID: x.info.PID, Floating: w.floating, Width: w.floatW, Height: w.floatH, Parent: parent, Positioned: true, Modal: w.modal, Remap: w.gen > 1}) | |||
| 284 | + | if x.info.Fullscreen { | |||
| 285 | + | s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true}) | |||
| 286 | + | } | |||
| 287 | + | s.syncInhibitors() | |||
| 288 | + | s.toplevelChanged(w) | |||
| 289 | + | s.castInventoryChanged() | |||
| 290 | + | s.log.Info().Uint64("id", uint64(w.id)).Uint32("xid", x.xid).Str("app_id", w.appID).Bool("floating", w.floating).Msg("x11 window mapped") | |||
| 291 | + | // Called from a commit, the content follows right after; called from the | |||
| 292 | + | // pairing, the surface already shows a buffer and is published now. | |||
| 293 | + | if surf.has { | |||
| 294 | + | surf.resetInputEmission() | |||
| 295 | + | surf.redraw() | |||
| 296 | + | surf.emitInput() | |||
| 297 | + | } | |||
| 298 | + | } | |||
A internal/adapters/wayland/x11window_test.go +324 -0
| @@ -0,0 +1,324 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "net" | |
| 7 | + | "os" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 12 | + | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 13 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 14 | + | "github.com/bnema/neferwl/internal/ports" | |
| 15 | + | "github.com/bnema/wlturbo" | |
| 16 | + | "golang.org/x/sys/unix" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | var x11Serial = uint64(0x1234_0000_0042) | |
| 20 | + | ||
| 21 | + | // x11Rig is a server with an Xwayland client (a real wlturbo client on the | |
| 22 | + | // adopted descriptor), a scripted X peer on the WM descriptor and the XWM | |
| 23 | + | // between them. | |
| 24 | + | type x11Rig struct { | |
| 25 | + | t *testing.T | |
| 26 | + | s *Server | |
| 27 | + | events chan ports.ClientEvent | |
| 28 | + | commands chan ports.ClientCommand | |
| 29 | + | dir string | |
| 30 | + | xc *wlturbo.Display | |
| 31 | + | peer *x11Peer | |
| 32 | + | stop func() | |
| 33 | + | shell uint32 | |
| 34 | + | comp uint32 | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | func newX11Rig(t *testing.T) *x11Rig { | |
| 38 | + | t.Helper() | |
| 39 | + | s, events, commands, dir := lifecycleServer(t) | |
| 40 | + | r := &x11Rig{t: t, s: s, events: events, commands: commands, dir: dir} | |
| 41 | + | wl := xwaylandPair(t) | |
| 42 | + | s.AttachXwayland(wl[0]) | |
| 43 | + | f := os.NewFile(uintptr(wl[1]), "xwayland client") | |
| 44 | + | wl[1] = -1 | |
| 45 | + | conn, err := net.FileConn(f) | |
| 46 | + | f.Close() | |
| 47 | + | if err != nil { | |
| 48 | + | t.Fatal(err) | |
| 49 | + | } | |
| 50 | + | if r.xc, err = wlturbo.ConnectFromConn(conn); err != nil { | |
| 51 | + | t.Fatal(err) | |
| 52 | + | } | |
| 53 | + | t.Cleanup(func() { _ = r.xc.Close(); forgetWireClient(r.xc) }) | |
| 54 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 55 | + | t.Fatal(err) | |
| 56 | + | } | |
| 57 | + | wm, peer := startX11Peer(t) | |
| 58 | + | r.peer = peer | |
| 59 | + | r.stop = s.AttachXWM(context.Background(), wm) | |
| 60 | + | t.Cleanup(func() { r.stop() }) | |
| 61 | + | r.shell = bindProtocol(t, r.xc, "xwayland_shell_v1") | |
| 62 | + | r.comp = bindProtocol(t, r.xc, "wl_compositor") | |
| 63 | + | return r | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // x11Window announces and maps X window win with the properties of an | |
| 67 | + | // ordinary client, and waits for the XWM to be ready for it. | |
| 68 | + | func (r *x11Rig) x11Window(win uint32) { | |
| 69 | + | r.t.Helper() | |
| 70 | + | p := r.peer | |
| 71 | + | str := uint32(31) // XA_STRING | |
| 72 | + | p.setProp(win, 67, str, 8, []byte("inst\x00Class\x00")) // WM_CLASS | |
| 73 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("net title")) | |
| 74 | + | p.setProp(win, p.atom("_NET_WM_PID"), 6, 32, x11le32(4242)) | |
| 75 | + | p.createNotify(win) | |
| 76 | + | skip := len(p.requests()) | |
| 77 | + | p.mapRequest(win) | |
| 78 | + | p.waitReq(skip, func(q x11Req) bool { return q.major == 8 && q.u32(4) == win }) // MapWindow | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // pair sends the serial from both sides and commits a buffer on the surface. | |
| 82 | + | // It returns the wl_surface id of the Xwayland client. | |
| 83 | + | func (r *x11Rig) pair(win uint32, serial uint64) uint32 { | |
| 84 | + | r.t.Helper() | |
| 85 | + | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | |
| 86 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |
| 87 | + | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 88 | + | registerProtocol(r.t, r.xc, surf) | |
| 89 | + | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 90 | + | registerProtocol(r.t, r.xc, xs) | |
| 91 | + | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | |
| 92 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(r.t, r.xc), int32(0), int32(0)) | |
| 93 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestCommit) | |
| 94 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 95 | + | r.t.Fatal(err) | |
| 96 | + | } | |
| 97 | + | return surf | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func awaitEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |
| 101 | + | t.Helper() | |
| 102 | + | for deadline := time.After(3 * time.Second); ; { | |
| 103 | + | select { | |
| 104 | + | case ev := <-events: | |
| 105 | + | if v, ok := ev.(T); ok { | |
| 106 | + | return v | |
| 107 | + | } | |
| 108 | + | case <-deadline: | |
| 109 | + | var zero T | |
| 110 | + | t.Fatalf("no %T event", zero) | |
| 111 | + | } | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | func TestXwaylandGlobalFilter(t *testing.T) { | |
| 116 | + | s, _, _, dir := lifecycleServer(t) | |
| 117 | + | other := protocolClient(t, s, dir) | |
| 118 | + | for _, iface := range []string{"xwayland_shell_v1", "zwp_xwayland_keyboard_grab_manager_v1"} { | |
| 119 | + | if _, ok := other.Registry().FindGlobal(iface); ok { | |
| 120 | + | t.Errorf("ordinary client sees %s", iface) | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | if _, ok := other.Registry().FindGlobal("xdg_wm_base"); !ok { | |
| 124 | + | t.Error("ordinary globals filtered out") | |
| 125 | + | } | |
| 126 | + | r := newX11Rig(t) | |
| 127 | + | if _, ok := r.xc.Registry().FindGlobal("xwayland_shell_v1"); !ok { | |
| 128 | + | t.Error("Xwayland client misses xwayland_shell_v1") | |
| 129 | + | } | |
| 130 | + | // The ordinary client of the same server still does not see it. | |
| 131 | + | late := protocolClient(t, r.s, r.dir) | |
| 132 | + | if _, ok := late.Registry().FindGlobal("xwayland_shell_v1"); ok { | |
| 133 | + | t.Error("ordinary client sees xwayland_shell_v1 while Xwayland is attached") | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func TestXwaylandShellRoleErrors(t *testing.T) { | |
| 138 | + | for _, scenario := range []string{"xdg role first", "serial twice", "zero serial"} { | |
| 139 | + | t.Run(scenario, func(t *testing.T) { | |
| 140 | + | r := newX11Rig(t) | |
| 141 | + | c := r.xc | |
| 142 | + | wmBase := bindProtocol(t, c, "xdg_wm_base") | |
| 143 | + | surf := c.AllocateID() | |
| 144 | + | requestProtocol(t, c, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 145 | + | registerProtocol(t, c, surf) | |
| 146 | + | xs := c.AllocateID() | |
| 147 | + | switch scenario { | |
| 148 | + | case "xdg role first": | |
| 149 | + | xdg := xs | |
| 150 | + | requestProtocol(t, c, wmBase, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 151 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, c.AllocateID(), surf) | |
| 152 | + | expectProtocolError(t, c, r.shell, uint32(xwaylandshell.XwaylandShellV1ErrorRole)) | |
| 153 | + | case "serial twice": | |
| 154 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 155 | + | registerProtocol(t, c, xs) | |
| 156 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(1), uint32(0)) | |
| 157 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(2), uint32(0)) | |
| 158 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated)) | |
| 159 | + | case "zero serial": | |
| 160 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 161 | + | registerProtocol(t, c, xs) | |
| 162 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(0), uint32(0)) | |
| 163 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial)) | |
| 164 | + | } | |
| 165 | + | }) | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | // A window announced by the X server and a surface carrying the same serial | |
| 170 | + | // map as one neferwl window with the X11 info; core commands reach the X side. | |
| 171 | + | func TestX11WindowLifecycle(t *testing.T) { | |
| 172 | + | r := newX11Rig(t) | |
| 173 | + | win := uint32(x11Win) | |
| 174 | + | r.x11Window(win) | |
| 175 | + | surf := r.pair(win, x11Serial) | |
| 176 | + | ||
| 177 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 178 | + | if m.AppID != "Class" || m.PID != 4242 || !m.Positioned || m.Floating || m.Slot != "" { | |
| 179 | + | t.Fatalf("mapped %+v", m) | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // Title and app id changes. | |
| 183 | + | p := r.peer | |
| 184 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("renamed")) | |
| 185 | + | p.propertyNotify(win, p.atom("_NET_WM_NAME")) | |
| 186 | + | p.setProp(win, 67, 31, 8, []byte("inst\x00Other\x00")) | |
| 187 | + | p.propertyNotify(win, 67) | |
| 188 | + | if a := awaitEvent[ports.WindowAppID](t, r.events); a.ID != m.ID || a.AppID != "Other" { | |
| 189 | + | t.Fatalf("app id %+v", a) | |
| 190 | + | } | |
| 191 | + | var title string | |
| 192 | + | waitTrue(t, "title", func() bool { | |
| 193 | + | r.s.display.Do(func() { | |
| 194 | + | if w := r.s.windows[m.ID]; w != nil { | |
| 195 | + | title = w.title | |
| 196 | + | } | |
| 197 | + | }) | |
| 198 | + | return title == "renamed" | |
| 199 | + | }) | |
| 200 | + | ||
| 201 | + | // Fullscreen request from the client. | |
| 202 | + | p.clientMessage(win, "_NET_WM_STATE", 1, p.atom("_NET_WM_STATE_FULLSCREEN")) | |
| 203 | + | if f := awaitEvent[ports.WindowFullscreenRequest](t, r.events); f.ID != m.ID || !f.Fullscreen || f.External { | |
| 204 | + | t.Fatalf("fullscreen %+v", f) | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | // Configure and Place reach the X server as ConfigureWindow. | |
| 208 | + | skip := len(p.requests()) | |
| 209 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 640, Height: 360, Visible: true} | |
| 210 | + | cfg, skip := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |
| 211 | + | if w := binary.LittleEndian.Uint32(cfg.raw[20:]); w != 640 { | |
| 212 | + | t.Fatalf("configured width %d, want 640 (%x)", w, cfg.raw) | |
| 213 | + | } | |
| 214 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 10, Y: 20} | |
| 215 | + | cfg, _ = p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |
| 216 | + | if x, y := int32(binary.LittleEndian.Uint32(cfg.raw[12:])), int32(binary.LittleEndian.Uint32(cfg.raw[16:])); x != 10 || y != 20 { | |
| 217 | + | t.Fatalf("placed at %d,%d, want 10,20", x, y) | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | // Focus. | |
| 221 | + | skip = len(p.requests()) | |
| 222 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 223 | + | if f, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) != win { | |
| 224 | + | t.Fatalf("focus on %#x, want %#x", f.u32(4), win) | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | // Close: without WM_DELETE_WINDOW the client is killed. | |
| 228 | + | skip = len(p.requests()) | |
| 229 | + | r.commands <- ports.CloseWindow{ID: m.ID} | |
| 230 | + | if k, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpKillClient }); k.u32(4) != win { | |
| 231 | + | t.Fatalf("killed %#x, want %#x", k.u32(4), win) | |
| 232 | + | } | |
| 233 | + | ||
| 234 | + | // Destroying the surface unmaps the window. | |
| 235 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |
| 236 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 237 | + | t.Fatal(err) | |
| 238 | + | } | |
| 239 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 240 | + | t.Fatalf("unmapped %+v", u) | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | // The serial may arrive on the Wayland side before the X side knows it, and | |
| 245 | + | // an UnmapNotify unmaps; a remap pairs again. | |
| 246 | + | func TestX11WindowSurfaceFirstAndRemap(t *testing.T) { | |
| 247 | + | r := newX11Rig(t) | |
| 248 | + | win := uint32(x11Win) | |
| 249 | + | p := r.peer | |
| 250 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |
| 251 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 252 | + | registerProtocol(t, r.xc, surf) | |
| 253 | + | requestProtocol(t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 254 | + | registerProtocol(t, r.xc, xs) | |
| 255 | + | requestProtocol(t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(x11Serial), uint32(x11Serial>>32)) | |
| 256 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(t, r.xc), int32(0), int32(0)) | |
| 257 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestCommit) | |
| 258 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 259 | + | t.Fatal(err) | |
| 260 | + | } | |
| 261 | + | r.x11Window(win) | |
| 262 | + | p.clientMessage(win, "WL_SURFACE_SERIAL", uint32(x11Serial), uint32(x11Serial>>32)) | |
| 263 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 264 | + | if m.Remap { | |
| 265 | + | t.Fatalf("first map flagged as remap: %+v", m) | |
| 266 | + | } | |
| 267 | + | p.unmapNotify(win) | |
| 268 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 269 | + | t.Fatalf("unmapped %+v", u) | |
| 270 | + | } | |
| 271 | + | p.destroyNotify(win) | |
| 272 | + | waitTrue(t, "window forgotten", func() bool { | |
| 273 | + | gone := false | |
| 274 | + | r.s.display.Do(func() { gone = r.s.windows[m.ID] == nil }) | |
| 275 | + | return gone | |
| 276 | + | }) | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | // stop closes the XWM: its windows unmap, and the server keeps serving. | |
| 280 | + | func TestX11StopUnmapsWindows(t *testing.T) { | |
| 281 | + | r := newX11Rig(t) | |
| 282 | + | win := uint32(x11Win) | |
| 283 | + | r.x11Window(win) | |
| 284 | + | r.pair(win, x11Serial) | |
| 285 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 286 | + | r.stop() | |
| 287 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 288 | + | t.Fatalf("unmapped %+v", u) | |
| 289 | + | } | |
| 290 | + | r.s.display.Do(func() { | |
| 291 | + | if r.s.xwm != nil || r.s.windows[m.ID] != nil || len(r.s.x11Slots) != 0 { | |
| 292 | + | t.Error("XWM state left behind") | |
| 293 | + | } | |
| 294 | + | }) | |
| 295 | + | r.stop() // idempotent | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | // Boot gives up as soon as its context ends, even against a silent server, | |
| 299 | + | // and leaves the server serving. | |
| 300 | + | func TestAttachXWMBootAbortsWithContext(t *testing.T) { | |
| 301 | + | s, _, _, dir := lifecycleServer(t) | |
| 302 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 303 | + | if err != nil { | |
| 304 | + | t.Fatal(err) | |
| 305 | + | } | |
| 306 | + | defer unix.Close(fds[1]) // the silent server end | |
| 307 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 308 | + | done := make(chan func(), 1) | |
| 309 | + | go func() { done <- s.AttachXWM(ctx, fds[0]) }() | |
| 310 | + | select { | |
| 311 | + | case <-done: | |
| 312 | + | t.Fatal("AttachXWM returned before the context ended") | |
| 313 | + | case <-time.After(150 * time.Millisecond): | |
| 314 | + | } | |
| 315 | + | cancel() | |
| 316 | + | select { | |
| 317 | + | case stop := <-done: | |
| 318 | + | stop() | |
| 319 | + | case <-time.After(2 * time.Second): | |
| 320 | + | t.Fatal("AttachXWM did not abort with its context") | |
| 321 | + | } | |
| 322 | + | assertFDClosed(t, fds[0]) | |
| 323 | + | protocolClient(t, s, dir) // still serving | |
| 324 | + | } | |
| @@ -0,0 +1,324 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "net" | |||
| 7 | + | "os" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 12 | + | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |||
| 13 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 14 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 15 | + | "github.com/bnema/wlturbo" | |||
| 16 | + | "golang.org/x/sys/unix" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | var x11Serial = uint64(0x1234_0000_0042) | |||
| 20 | + | ||||
| 21 | + | // x11Rig is a server with an Xwayland client (a real wlturbo client on the | |||
| 22 | + | // adopted descriptor), a scripted X peer on the WM descriptor and the XWM | |||
| 23 | + | // between them. | |||
| 24 | + | type x11Rig struct { | |||
| 25 | + | t *testing.T | |||
| 26 | + | s *Server | |||
| 27 | + | events chan ports.ClientEvent | |||
| 28 | + | commands chan ports.ClientCommand | |||
| 29 | + | dir string | |||
| 30 | + | xc *wlturbo.Display | |||
| 31 | + | peer *x11Peer | |||
| 32 | + | stop func() | |||
| 33 | + | shell uint32 | |||
| 34 | + | comp uint32 | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | func newX11Rig(t *testing.T) *x11Rig { | |||
| 38 | + | t.Helper() | |||
| 39 | + | s, events, commands, dir := lifecycleServer(t) | |||
| 40 | + | r := &x11Rig{t: t, s: s, events: events, commands: commands, dir: dir} | |||
| 41 | + | wl := xwaylandPair(t) | |||
| 42 | + | s.AttachXwayland(wl[0]) | |||
| 43 | + | f := os.NewFile(uintptr(wl[1]), "xwayland client") | |||
| 44 | + | wl[1] = -1 | |||
| 45 | + | conn, err := net.FileConn(f) | |||
| 46 | + | f.Close() | |||
| 47 | + | if err != nil { | |||
| 48 | + | t.Fatal(err) | |||
| 49 | + | } | |||
| 50 | + | if r.xc, err = wlturbo.ConnectFromConn(conn); err != nil { | |||
| 51 | + | t.Fatal(err) | |||
| 52 | + | } | |||
| 53 | + | t.Cleanup(func() { _ = r.xc.Close(); forgetWireClient(r.xc) }) | |||
| 54 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 55 | + | t.Fatal(err) | |||
| 56 | + | } | |||
| 57 | + | wm, peer := startX11Peer(t) | |||
| 58 | + | r.peer = peer | |||
| 59 | + | r.stop = s.AttachXWM(context.Background(), wm) | |||
| 60 | + | t.Cleanup(func() { r.stop() }) | |||
| 61 | + | r.shell = bindProtocol(t, r.xc, "xwayland_shell_v1") | |||
| 62 | + | r.comp = bindProtocol(t, r.xc, "wl_compositor") | |||
| 63 | + | return r | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // x11Window announces and maps X window win with the properties of an | |||
| 67 | + | // ordinary client, and waits for the XWM to be ready for it. | |||
| 68 | + | func (r *x11Rig) x11Window(win uint32) { | |||
| 69 | + | r.t.Helper() | |||
| 70 | + | p := r.peer | |||
| 71 | + | str := uint32(31) // XA_STRING | |||
| 72 | + | p.setProp(win, 67, str, 8, []byte("inst\x00Class\x00")) // WM_CLASS | |||
| 73 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("net title")) | |||
| 74 | + | p.setProp(win, p.atom("_NET_WM_PID"), 6, 32, x11le32(4242)) | |||
| 75 | + | p.createNotify(win) | |||
| 76 | + | skip := len(p.requests()) | |||
| 77 | + | p.mapRequest(win) | |||
| 78 | + | p.waitReq(skip, func(q x11Req) bool { return q.major == 8 && q.u32(4) == win }) // MapWindow | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // pair sends the serial from both sides and commits a buffer on the surface. | |||
| 82 | + | // It returns the wl_surface id of the Xwayland client. | |||
| 83 | + | func (r *x11Rig) pair(win uint32, serial uint64) uint32 { | |||
| 84 | + | r.t.Helper() | |||
| 85 | + | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | |||
| 86 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |||
| 87 | + | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 88 | + | registerProtocol(r.t, r.xc, surf) | |||
| 89 | + | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 90 | + | registerProtocol(r.t, r.xc, xs) | |||
| 91 | + | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | |||
| 92 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(r.t, r.xc), int32(0), int32(0)) | |||
| 93 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestCommit) | |||
| 94 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 95 | + | r.t.Fatal(err) | |||
| 96 | + | } | |||
| 97 | + | return surf | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | func awaitEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |||
| 101 | + | t.Helper() | |||
| 102 | + | for deadline := time.After(3 * time.Second); ; { | |||
| 103 | + | select { | |||
| 104 | + | case ev := <-events: | |||
| 105 | + | if v, ok := ev.(T); ok { | |||
| 106 | + | return v | |||
| 107 | + | } | |||
| 108 | + | case <-deadline: | |||
| 109 | + | var zero T | |||
| 110 | + | t.Fatalf("no %T event", zero) | |||
| 111 | + | } | |||
| 112 | + | } | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | func TestXwaylandGlobalFilter(t *testing.T) { | |||
| 116 | + | s, _, _, dir := lifecycleServer(t) | |||
| 117 | + | other := protocolClient(t, s, dir) | |||
| 118 | + | for _, iface := range []string{"xwayland_shell_v1", "zwp_xwayland_keyboard_grab_manager_v1"} { | |||
| 119 | + | if _, ok := other.Registry().FindGlobal(iface); ok { | |||
| 120 | + | t.Errorf("ordinary client sees %s", iface) | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | if _, ok := other.Registry().FindGlobal("xdg_wm_base"); !ok { | |||
| 124 | + | t.Error("ordinary globals filtered out") | |||
| 125 | + | } | |||
| 126 | + | r := newX11Rig(t) | |||
| 127 | + | if _, ok := r.xc.Registry().FindGlobal("xwayland_shell_v1"); !ok { | |||
| 128 | + | t.Error("Xwayland client misses xwayland_shell_v1") | |||
| 129 | + | } | |||
| 130 | + | // The ordinary client of the same server still does not see it. | |||
| 131 | + | late := protocolClient(t, r.s, r.dir) | |||
| 132 | + | if _, ok := late.Registry().FindGlobal("xwayland_shell_v1"); ok { | |||
| 133 | + | t.Error("ordinary client sees xwayland_shell_v1 while Xwayland is attached") | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | func TestXwaylandShellRoleErrors(t *testing.T) { | |||
| 138 | + | for _, scenario := range []string{"xdg role first", "serial twice", "zero serial"} { | |||
| 139 | + | t.Run(scenario, func(t *testing.T) { | |||
| 140 | + | r := newX11Rig(t) | |||
| 141 | + | c := r.xc | |||
| 142 | + | wmBase := bindProtocol(t, c, "xdg_wm_base") | |||
| 143 | + | surf := c.AllocateID() | |||
| 144 | + | requestProtocol(t, c, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 145 | + | registerProtocol(t, c, surf) | |||
| 146 | + | xs := c.AllocateID() | |||
| 147 | + | switch scenario { | |||
| 148 | + | case "xdg role first": | |||
| 149 | + | xdg := xs | |||
| 150 | + | requestProtocol(t, c, wmBase, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |||
| 151 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, c.AllocateID(), surf) | |||
| 152 | + | expectProtocolError(t, c, r.shell, uint32(xwaylandshell.XwaylandShellV1ErrorRole)) | |||
| 153 | + | case "serial twice": | |||
| 154 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 155 | + | registerProtocol(t, c, xs) | |||
| 156 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(1), uint32(0)) | |||
| 157 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(2), uint32(0)) | |||
| 158 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated)) | |||
| 159 | + | case "zero serial": | |||
| 160 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 161 | + | registerProtocol(t, c, xs) | |||
| 162 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(0), uint32(0)) | |||
| 163 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial)) | |||
| 164 | + | } | |||
| 165 | + | }) | |||
| 166 | + | } | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | // A window announced by the X server and a surface carrying the same serial | |||
| 170 | + | // map as one neferwl window with the X11 info; core commands reach the X side. | |||
| 171 | + | func TestX11WindowLifecycle(t *testing.T) { | |||
| 172 | + | r := newX11Rig(t) | |||
| 173 | + | win := uint32(x11Win) | |||
| 174 | + | r.x11Window(win) | |||
| 175 | + | surf := r.pair(win, x11Serial) | |||
| 176 | + | ||||
| 177 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 178 | + | if m.AppID != "Class" || m.PID != 4242 || !m.Positioned || m.Floating || m.Slot != "" { | |||
| 179 | + | t.Fatalf("mapped %+v", m) | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // Title and app id changes. | |||
| 183 | + | p := r.peer | |||
| 184 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("renamed")) | |||
| 185 | + | p.propertyNotify(win, p.atom("_NET_WM_NAME")) | |||
| 186 | + | p.setProp(win, 67, 31, 8, []byte("inst\x00Other\x00")) | |||
| 187 | + | p.propertyNotify(win, 67) | |||
| 188 | + | if a := awaitEvent[ports.WindowAppID](t, r.events); a.ID != m.ID || a.AppID != "Other" { | |||
| 189 | + | t.Fatalf("app id %+v", a) | |||
| 190 | + | } | |||
| 191 | + | var title string | |||
| 192 | + | waitTrue(t, "title", func() bool { | |||
| 193 | + | r.s.display.Do(func() { | |||
| 194 | + | if w := r.s.windows[m.ID]; w != nil { | |||
| 195 | + | title = w.title | |||
| 196 | + | } | |||
| 197 | + | }) | |||
| 198 | + | return title == "renamed" | |||
| 199 | + | }) | |||
| 200 | + | ||||
| 201 | + | // Fullscreen request from the client. | |||
| 202 | + | p.clientMessage(win, "_NET_WM_STATE", 1, p.atom("_NET_WM_STATE_FULLSCREEN")) | |||
| 203 | + | if f := awaitEvent[ports.WindowFullscreenRequest](t, r.events); f.ID != m.ID || !f.Fullscreen || f.External { | |||
| 204 | + | t.Fatalf("fullscreen %+v", f) | |||
| 205 | + | } | |||
| 206 | + | ||||
| 207 | + | // Configure and Place reach the X server as ConfigureWindow. | |||
| 208 | + | skip := len(p.requests()) | |||
| 209 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 640, Height: 360, Visible: true} | |||
| 210 | + | cfg, skip := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |||
| 211 | + | if w := binary.LittleEndian.Uint32(cfg.raw[20:]); w != 640 { | |||
| 212 | + | t.Fatalf("configured width %d, want 640 (%x)", w, cfg.raw) | |||
| 213 | + | } | |||
| 214 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 10, Y: 20} | |||
| 215 | + | cfg, _ = p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |||
| 216 | + | if x, y := int32(binary.LittleEndian.Uint32(cfg.raw[12:])), int32(binary.LittleEndian.Uint32(cfg.raw[16:])); x != 10 || y != 20 { | |||
| 217 | + | t.Fatalf("placed at %d,%d, want 10,20", x, y) | |||
| 218 | + | } | |||
| 219 | + | ||||
| 220 | + | // Focus. | |||
| 221 | + | skip = len(p.requests()) | |||
| 222 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |||
| 223 | + | if f, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) != win { | |||
| 224 | + | t.Fatalf("focus on %#x, want %#x", f.u32(4), win) | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | // Close: without WM_DELETE_WINDOW the client is killed. | |||
| 228 | + | skip = len(p.requests()) | |||
| 229 | + | r.commands <- ports.CloseWindow{ID: m.ID} | |||
| 230 | + | if k, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpKillClient }); k.u32(4) != win { | |||
| 231 | + | t.Fatalf("killed %#x, want %#x", k.u32(4), win) | |||
| 232 | + | } | |||
| 233 | + | ||||
| 234 | + | // Destroying the surface unmaps the window. | |||
| 235 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |||
| 236 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 237 | + | t.Fatal(err) | |||
| 238 | + | } | |||
| 239 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 240 | + | t.Fatalf("unmapped %+v", u) | |||
| 241 | + | } | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | // The serial may arrive on the Wayland side before the X side knows it, and | |||
| 245 | + | // an UnmapNotify unmaps; a remap pairs again. | |||
| 246 | + | func TestX11WindowSurfaceFirstAndRemap(t *testing.T) { | |||
| 247 | + | r := newX11Rig(t) | |||
| 248 | + | win := uint32(x11Win) | |||
| 249 | + | p := r.peer | |||
| 250 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |||
| 251 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |||
| 252 | + | registerProtocol(t, r.xc, surf) | |||
| 253 | + | requestProtocol(t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |||
| 254 | + | registerProtocol(t, r.xc, xs) | |||
| 255 | + | requestProtocol(t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(x11Serial), uint32(x11Serial>>32)) | |||
| 256 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(t, r.xc), int32(0), int32(0)) | |||
| 257 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestCommit) | |||
| 258 | + | if err := r.xc.Roundtrip(); err != nil { | |||
| 259 | + | t.Fatal(err) | |||
| 260 | + | } | |||
| 261 | + | r.x11Window(win) | |||
| 262 | + | p.clientMessage(win, "WL_SURFACE_SERIAL", uint32(x11Serial), uint32(x11Serial>>32)) | |||
| 263 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 264 | + | if m.Remap { | |||
| 265 | + | t.Fatalf("first map flagged as remap: %+v", m) | |||
| 266 | + | } | |||
| 267 | + | p.unmapNotify(win) | |||
| 268 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 269 | + | t.Fatalf("unmapped %+v", u) | |||
| 270 | + | } | |||
| 271 | + | p.destroyNotify(win) | |||
| 272 | + | waitTrue(t, "window forgotten", func() bool { | |||
| 273 | + | gone := false | |||
| 274 | + | r.s.display.Do(func() { gone = r.s.windows[m.ID] == nil }) | |||
| 275 | + | return gone | |||
| 276 | + | }) | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | // stop closes the XWM: its windows unmap, and the server keeps serving. | |||
| 280 | + | func TestX11StopUnmapsWindows(t *testing.T) { | |||
| 281 | + | r := newX11Rig(t) | |||
| 282 | + | win := uint32(x11Win) | |||
| 283 | + | r.x11Window(win) | |||
| 284 | + | r.pair(win, x11Serial) | |||
| 285 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |||
| 286 | + | r.stop() | |||
| 287 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |||
| 288 | + | t.Fatalf("unmapped %+v", u) | |||
| 289 | + | } | |||
| 290 | + | r.s.display.Do(func() { | |||
| 291 | + | if r.s.xwm != nil || r.s.windows[m.ID] != nil || len(r.s.x11Slots) != 0 { | |||
| 292 | + | t.Error("XWM state left behind") | |||
| 293 | + | } | |||
| 294 | + | }) | |||
| 295 | + | r.stop() // idempotent | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | // Boot gives up as soon as its context ends, even against a silent server, | |||
| 299 | + | // and leaves the server serving. | |||
| 300 | + | func TestAttachXWMBootAbortsWithContext(t *testing.T) { | |||
| 301 | + | s, _, _, dir := lifecycleServer(t) | |||
| 302 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 303 | + | if err != nil { | |||
| 304 | + | t.Fatal(err) | |||
| 305 | + | } | |||
| 306 | + | defer unix.Close(fds[1]) // the silent server end | |||
| 307 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 308 | + | done := make(chan func(), 1) | |||
| 309 | + | go func() { done <- s.AttachXWM(ctx, fds[0]) }() | |||
| 310 | + | select { | |||
| 311 | + | case <-done: | |||
| 312 | + | t.Fatal("AttachXWM returned before the context ended") | |||
| 313 | + | case <-time.After(150 * time.Millisecond): | |||
| 314 | + | } | |||
| 315 | + | cancel() | |||
| 316 | + | select { | |||
| 317 | + | case stop := <-done: | |||
| 318 | + | stop() | |||
| 319 | + | case <-time.After(2 * time.Second): | |||
| 320 | + | t.Fatal("AttachXWM did not abort with its context") | |||
| 321 | + | } | |||
| 322 | + | assertFDClosed(t, fds[0]) | |||
| 323 | + | protocolClient(t, s, dir) // still serving | |||
| 324 | + | } | |||
M internal/adapters/wayland/xdg.go +30 -19
| @@ -128,10 +128,12 @@ func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | |||
| 128 | 128 | ||
| 129 | 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | |
| 130 | 130 | type window struct { | |
| 131 | - | id ports.WindowID | |
| 132 | - | toplevel *xdgshell.Toplevel | |
| 133 | - | popup *popup | |
| 134 | - | xdg *xdgSurface | |
| 131 | + | id ports.WindowID | |
| 132 | + | toplevel *xdgshell.Toplevel | |
| 133 | + | popup *popup | |
| 134 | + | xdg *xdgSurface | |
| 135 | + | // x11 is set for the window of an X11 client (x11window.go). | |
| 136 | + | x11 *x11Window | |
| 135 | 137 | appID, title string | |
| 136 | 138 | mapped bool | |
| 137 | 139 | // gen counts the mappings: an unmap and a remap is a new toplevel to | |
| @@ -192,24 +194,29 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte { | |||
| 192 | 194 | } | |
| 193 | 195 | ||
| 194 | 196 | func (w *window) unmap() { | |
| 195 | - | if w.xdg.server.seat.pointerFocus == w.id { | |
| 196 | - | w.xdg.server.changePointerFocus(0, 0, 0) | |
| 197 | + | s := w.srv() | |
| 198 | + | if s.seat.pointerFocus == w.id { | |
| 199 | + | s.changePointerFocus(0, 0, 0) | |
| 197 | 200 | } | |
| 198 | - | if w.xdg.server.seat.focused == w.id { | |
| 199 | - | w.xdg.server.changeFocus(0) | |
| 201 | + | if s.seat.focused == w.id { | |
| 202 | + | s.changeFocus(0) | |
| 200 | 203 | } | |
| 201 | 204 | if !w.mapped { | |
| 202 | 205 | return | |
| 203 | 206 | } | |
| 204 | 207 | w.mapped = false | |
| 205 | - | w.xdg.server.toplevelClosed(w.id) | |
| 206 | - | w.xdg.surface.resetInputEmission() | |
| 207 | - | w.xdg.configured = false | |
| 208 | - | w.xdg.acked = false | |
| 209 | - | w.xdg.serials = nil | |
| 210 | - | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | |
| 211 | - | w.xdg.server.syncInhibitors() | |
| 212 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |
| 208 | + | s.toplevelClosed(w.id) | |
| 209 | + | if surf := w.surf(); surf != nil { | |
| 210 | + | surf.resetInputEmission() | |
| 211 | + | } | |
| 212 | + | if x := w.xdg; x != nil { | |
| 213 | + | x.configured = false | |
| 214 | + | x.acked = false | |
| 215 | + | x.serials = nil | |
| 216 | + | } | |
| 217 | + | s.emit(ports.WindowUnmapped{ID: w.id}) | |
| 218 | + | s.syncInhibitors() | |
| 219 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |
| 213 | 220 | } | |
| 214 | 221 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | |
| 215 | 222 | if x.window != nil { | |
| @@ -337,7 +344,7 @@ func (w *window) setParent(p *window) { | |||
| 337 | 344 | if p != nil && p.mapped { | |
| 338 | 345 | id = p.id | |
| 339 | 346 | } | |
| 340 | - | w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id}) | |
| 347 | + | w.srv().emit(ports.WindowParent{ID: w.id, Parent: id}) | |
| 341 | 348 | } | |
| 342 | 349 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | |
| 343 | 350 | if title == t.w.title { | |
| @@ -417,7 +424,7 @@ func (w *window) afterCommit() { | |||
| 417 | 424 | } | |
| 418 | 425 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | |
| 419 | 426 | w.floatW, w.floatH = fw, fh | |
| 420 | - | w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) | |
| 427 | + | w.srv().emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) | |
| 421 | 428 | } | |
| 422 | 429 | } | |
| 423 | 430 | ||
| @@ -431,6 +438,10 @@ func (w *window) floats() bool { | |||
| 431 | 438 | ||
| 432 | 439 | // size is the window geometry size, else the surface size, in logical pixels. | |
| 433 | 440 | func (w *window) size() (int, int) { | |
| 441 | + | if w.xdg == nil { | |
| 442 | + | c := w.surf().content | |
| 443 | + | return c.LogicalW, c.LogicalH | |
| 444 | + | } | |
| 434 | 445 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | |
| 435 | 446 | return g.W, g.H | |
| 436 | 447 | } | |
| @@ -441,7 +452,7 @@ func (w *window) size() (int, int) { | |||
| 441 | 452 | // surfacePoint turns window coordinates (core's, from the geometry origin) | |
| 442 | 453 | // into surface coordinates. | |
| 443 | 454 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | |
| 444 | - | g := w.xdg.geometry | |
| 455 | + | g := w.geometry() | |
| 445 | 456 | return x + float64(g.X), y + float64(g.Y) | |
| 446 | 457 | } | |
| 447 | 458 | ||
| @@ -128,10 +128,12 @@ func (x *xdgSurface) Destroy(r *xdgshell.Surface) { | |||||
| 128 | 128 | ||||
| 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | ||
| 130 | type window struct { | 130 | type window struct { | ||
| 131 | - | id ports.WindowID | 131 | + | id ports.WindowID |
| 132 | - | toplevel *xdgshell.Toplevel | 132 | + | toplevel *xdgshell.Toplevel |
| 133 | - | popup *popup | 133 | + | popup *popup |
| 134 | - | xdg *xdgSurface | 134 | + | xdg *xdgSurface |
| 135 | + | // x11 is set for the window of an X11 client (x11window.go). | |||
| 136 | + | x11 *x11Window | |||
| 135 | appID, title string | 137 | appID, title string | ||
| 136 | mapped bool | 138 | mapped bool | ||
| 137 | // gen counts the mappings: an unmap and a remap is a new toplevel to | 139 | // gen counts the mappings: an unmap and a remap is a new toplevel to | ||
| @@ -192,24 +194,29 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte { | |||||
| 192 | } | 194 | } | ||
| 193 | 195 | ||||
| 194 | func (w *window) unmap() { | 196 | func (w *window) unmap() { | ||
| 195 | - | if w.xdg.server.seat.pointerFocus == w.id { | 197 | + | s := w.srv() |
| 196 | - | w.xdg.server.changePointerFocus(0, 0, 0) | 198 | + | if s.seat.pointerFocus == w.id { |
| 199 | + | s.changePointerFocus(0, 0, 0) | |||
| 197 | } | 200 | } | ||
| 198 | - | if w.xdg.server.seat.focused == w.id { | 201 | + | if s.seat.focused == w.id { |
| 199 | - | w.xdg.server.changeFocus(0) | 202 | + | s.changeFocus(0) |
| 200 | } | 203 | } | ||
| 201 | if !w.mapped { | 204 | if !w.mapped { | ||
| 202 | return | 205 | return | ||
| 203 | } | 206 | } | ||
| 204 | w.mapped = false | 207 | w.mapped = false | ||
| 205 | - | w.xdg.server.toplevelClosed(w.id) | 208 | + | s.toplevelClosed(w.id) |
| 206 | - | w.xdg.surface.resetInputEmission() | 209 | + | if surf := w.surf(); surf != nil { |
| 207 | - | w.xdg.configured = false | 210 | + | surf.resetInputEmission() |
| 208 | - | w.xdg.acked = false | 211 | + | } |
| 209 | - | w.xdg.serials = nil | 212 | + | if x := w.xdg; x != nil { |
| 210 | - | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 213 | + | x.configured = false |
| 211 | - | w.xdg.server.syncInhibitors() | 214 | + | x.acked = false |
| 212 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | 215 | + | x.serials = nil |
| 216 | + | } | |||
| 217 | + | s.emit(ports.WindowUnmapped{ID: w.id}) | |||
| 218 | + | s.syncInhibitors() | |||
| 219 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |||
| 213 | } | 220 | } | ||
| 214 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 221 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| 215 | if x.window != nil { | 222 | if x.window != nil { | ||
| @@ -337,7 +344,7 @@ func (w *window) setParent(p *window) { | |||||
| 337 | if p != nil && p.mapped { | 344 | if p != nil && p.mapped { | ||
| 338 | id = p.id | 345 | id = p.id | ||
| 339 | } | 346 | } | ||
| 340 | - | w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id}) | 347 | + | w.srv().emit(ports.WindowParent{ID: w.id, Parent: id}) |
| 341 | } | 348 | } | ||
| 342 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | 349 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | ||
| 343 | if title == t.w.title { | 350 | if title == t.w.title { | ||
| @@ -417,7 +424,7 @@ func (w *window) afterCommit() { | |||||
| 417 | } | 424 | } | ||
| 418 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | 425 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | ||
| 419 | w.floatW, w.floatH = fw, fh | 426 | w.floatW, w.floatH = fw, fh | ||
| 420 | - | w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) | 427 | + | w.srv().emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) |
| 421 | } | 428 | } | ||
| 422 | } | 429 | } | ||
| 423 | 430 | ||||
| @@ -431,6 +438,10 @@ func (w *window) floats() bool { | |||||
| 431 | 438 | ||||
| 432 | // size is the window geometry size, else the surface size, in logical pixels. | 439 | // size is the window geometry size, else the surface size, in logical pixels. | ||
| 433 | func (w *window) size() (int, int) { | 440 | func (w *window) size() (int, int) { | ||
| 441 | + | if w.xdg == nil { | |||
| 442 | + | c := w.surf().content | |||
| 443 | + | return c.LogicalW, c.LogicalH | |||
| 444 | + | } | |||
| 434 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | 445 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | ||
| 435 | return g.W, g.H | 446 | return g.W, g.H | ||
| 436 | } | 447 | } | ||
| @@ -441,7 +452,7 @@ func (w *window) size() (int, int) { | |||||
| 441 | // surfacePoint turns window coordinates (core's, from the geometry origin) | 452 | // surfacePoint turns window coordinates (core's, from the geometry origin) | ||
| 442 | // into surface coordinates. | 453 | // into surface coordinates. | ||
| 443 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | 454 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | ||
| 444 | - | g := w.xdg.geometry | 455 | + | g := w.geometry() |
| 445 | return x + float64(g.X), y + float64(g.Y) | 456 | return x + float64(g.X), y + float64(g.Y) | ||
| 446 | } | 457 | } | ||
| 447 | 458 | ||||
M internal/adapters/wayland/xwayland.go +31 -7
| @@ -1,6 +1,10 @@ | |||
| 1 | 1 | package wayland | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 4 | 8 | "github.com/bnema/purego-libwayland/server" | |
| 5 | 9 | "golang.org/x/sys/unix" | |
| 6 | 10 | ) | |
| @@ -24,6 +28,7 @@ func (s *Server) AttachXwayland(wlFD int) { | |||
| 24 | 28 | s.xwayland = xwaylandClient{client: c} | |
| 25 | 29 | c.OnDestroy(func() { | |
| 26 | 30 | if s.xwayland.client == c { | |
| 31 | + | s.teardownXWM() | |
| 27 | 32 | s.forgetXwayland() | |
| 28 | 33 | } | |
| 29 | 34 | }) | |
| @@ -41,11 +46,30 @@ func (s *Server) forgetXwayland() { | |||
| 41 | 46 | } | |
| 42 | 47 | } | |
| 43 | 48 | ||
| 44 | - | // AttachXWM takes ownership of the window manager descriptor. It runs on the | |
| 45 | - | // starter goroutine once Xwayland is ready; the XWM is not implemented yet, | |
| 46 | - | // so the descriptor is closed and stop does nothing. | |
| 47 | - | func (s *Server) AttachXWM(wmFD int) (stop func()) { | |
| 48 | - | s.log.Info().Msg("X11 window management not implemented: closing XWM descriptor") | |
| 49 | - | _ = unix.Close(wmFD) | |
| 50 | - | return func() {} | |
| 49 | + | // AttachXWM bootstraps the X11 window manager on the window manager | |
| 50 | + | // descriptor, which it owns. It runs on the starter goroutine once Xwayland is | |
| 51 | + | // ready and blocks until the XWM is installed, ctx ends or Boot fails. stop | |
| 52 | + | // closes the XWM on the display goroutine and waits for it. | |
| 53 | + | func (s *Server) AttachXWM(ctx context.Context, wmFD int) (stop func()) { | |
| 54 | + | log := logging.For(ctx, "xwm") | |
| 55 | + | x, err := xwm.Boot(ctx, wmFD, log) | |
| 56 | + | if err != nil { | |
| 57 | + | if ctx.Err() != nil { | |
| 58 | + | log.Info().Err(err).Msg("X11 window management not started: Xwayland is going away") | |
| 59 | + | } else { | |
| 60 | + | log.Warn().Err(err).Msg("X11 window management disabled") | |
| 61 | + | } | |
| 62 | + | return func() {} | |
| 63 | + | } | |
| 64 | + | if !s.display.Do(func() { s.installXWM(x) }) { | |
| 65 | + | x.CloseConn() | |
| 66 | + | return func() {} | |
| 67 | + | } | |
| 68 | + | return func() { | |
| 69 | + | s.display.Do(func() { | |
| 70 | + | if s.xwm == x { | |
| 71 | + | s.teardownXWM() | |
| 72 | + | } | |
| 73 | + | }) | |
| 74 | + | } | |
| 51 | 75 | } | |
| @@ -1,6 +1,10 @@ | |||||
| 1 | package wayland | 1 | package wayland | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |||
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 4 | "github.com/bnema/purego-libwayland/server" | 8 | "github.com/bnema/purego-libwayland/server" | ||
| 5 | "golang.org/x/sys/unix" | 9 | "golang.org/x/sys/unix" | ||
| 6 | ) | 10 | ) | ||
| @@ -24,6 +28,7 @@ func (s *Server) AttachXwayland(wlFD int) { | |||||
| 24 | s.xwayland = xwaylandClient{client: c} | 28 | s.xwayland = xwaylandClient{client: c} | ||
| 25 | c.OnDestroy(func() { | 29 | c.OnDestroy(func() { | ||
| 26 | if s.xwayland.client == c { | 30 | if s.xwayland.client == c { | ||
| 31 | + | s.teardownXWM() | |||
| 27 | s.forgetXwayland() | 32 | s.forgetXwayland() | ||
| 28 | } | 33 | } | ||
| 29 | }) | 34 | }) | ||
| @@ -41,11 +46,30 @@ func (s *Server) forgetXwayland() { | |||||
| 41 | } | 46 | } | ||
| 42 | } | 47 | } | ||
| 43 | 48 | ||||
| 44 | - | // AttachXWM takes ownership of the window manager descriptor. It runs on the | 49 | + | // AttachXWM bootstraps the X11 window manager on the window manager |
| 45 | - | // starter goroutine once Xwayland is ready; the XWM is not implemented yet, | 50 | + | // descriptor, which it owns. It runs on the starter goroutine once Xwayland is |
| 46 | - | // so the descriptor is closed and stop does nothing. | 51 | + | // ready and blocks until the XWM is installed, ctx ends or Boot fails. stop |
| 47 | - | func (s *Server) AttachXWM(wmFD int) (stop func()) { | 52 | + | // closes the XWM on the display goroutine and waits for it. |
| 48 | - | s.log.Info().Msg("X11 window management not implemented: closing XWM descriptor") | 53 | + | func (s *Server) AttachXWM(ctx context.Context, wmFD int) (stop func()) { |
| 49 | - | _ = unix.Close(wmFD) | 54 | + | log := logging.For(ctx, "xwm") |
| 50 | - | return func() {} | 55 | + | x, err := xwm.Boot(ctx, wmFD, log) |
| 56 | + | if err != nil { | |||
| 57 | + | if ctx.Err() != nil { | |||
| 58 | + | log.Info().Err(err).Msg("X11 window management not started: Xwayland is going away") | |||
| 59 | + | } else { | |||
| 60 | + | log.Warn().Err(err).Msg("X11 window management disabled") | |||
| 61 | + | } | |||
| 62 | + | return func() {} | |||
| 63 | + | } | |||
| 64 | + | if !s.display.Do(func() { s.installXWM(x) }) { | |||
| 65 | + | x.CloseConn() | |||
| 66 | + | return func() {} | |||
| 67 | + | } | |||
| 68 | + | return func() { | |||
| 69 | + | s.display.Do(func() { | |||
| 70 | + | if s.xwm == x { | |||
| 71 | + | s.teardownXWM() | |||
| 72 | + | } | |||
| 73 | + | }) | |||
| 74 | + | } | |||
| 51 | } | 75 | } | ||
M internal/adapters/wayland/xwm/mapflow_test.go +2 -1
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package xwm | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "encoding/binary" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| @@ -262,7 +263,7 @@ func TestMapFlowNoXResAsksNothing(t *testing.T) { | |||
| 262 | 263 | exts := fullPeerExts() | |
| 263 | 264 | delete(exts, "X-Resource") | |
| 264 | 265 | fd, p := startXPeer(t, exts) | |
| 265 | - | x, err := Boot(fd, testLog()) | |
| 266 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 266 | 267 | require.NoError(t, err) | |
| 267 | 268 | t.Cleanup(x.CloseConn) | |
| 268 | 269 | h := newMockhost(t) | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package xwm | 1 | package xwm | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "encoding/binary" | 5 | "encoding/binary" | ||
| 5 | "testing" | 6 | "testing" | ||
| 6 | 7 | ||||
| @@ -262,7 +263,7 @@ func TestMapFlowNoXResAsksNothing(t *testing.T) { | |||||
| 262 | exts := fullPeerExts() | 263 | exts := fullPeerExts() | ||
| 263 | delete(exts, "X-Resource") | 264 | delete(exts, "X-Resource") | ||
| 264 | fd, p := startXPeer(t, exts) | 265 | fd, p := startXPeer(t, exts) | ||
| 265 | - | x, err := Boot(fd, testLog()) | 266 | + | x, err := Boot(context.Background(), fd, testLog()) |
| 266 | require.NoError(t, err) | 267 | require.NoError(t, err) | ||
| 267 | t.Cleanup(x.CloseConn) | 268 | t.Cleanup(x.CloseConn) | ||
| 268 | h := newMockhost(t) | 269 | h := newMockhost(t) | ||
M internal/adapters/wayland/xwm/xwm.go +42 -2
| @@ -8,10 +8,12 @@ | |||
| 8 | 8 | package xwm | |
| 9 | 9 | ||
| 10 | 10 | import ( | |
| 11 | + | "context" | |
| 11 | 12 | "encoding/binary" | |
| 12 | 13 | "errors" | |
| 13 | 14 | "fmt" | |
| 14 | 15 | "io" | |
| 16 | + | "os" | |
| 15 | 17 | ||
| 16 | 18 | "bnema.dev/xturbo" | |
| 17 | 19 | "bnema.dev/xturbo/composite" | |
| @@ -20,6 +22,7 @@ import ( | |||
| 20 | 22 | "bnema.dev/xturbo/xproto" | |
| 21 | 23 | "github.com/bnema/purego-libwayland/server" | |
| 22 | 24 | "github.com/bnema/zerowrap" | |
| 25 | + | "golang.org/x/sys/unix" | |
| 23 | 26 | ) | |
| 24 | 27 | ||
| 25 | 28 | var ( | |
| @@ -144,8 +147,45 @@ func (x *XWM) setProp32(win uint32, prop int, typ xproto.Atom, vals []uint32) { | |||
| 144 | 147 | // blocks (about 25 s at worst) and must not run on the display goroutine: the | |
| 145 | 148 | // handshake needs Xwayland, which needs the display goroutine to serve its | |
| 146 | 149 | // Wayland requests. It takes ownership of wmFD and closes it on failure. | |
| 147 | - | // log is scoped by the caller with logging.For(ctx, "xwm"). | |
| 148 | - | func Boot(wmFD int, log zerowrap.Logger) (*XWM, error) { | |
| 150 | + | // Boot gives up promptly when ctx ends (the Xwayland process died or the run | |
| 151 | + | // was cancelled). log is scoped by the caller with logging.For(ctx, "xwm"). | |
| 152 | + | func Boot(ctx context.Context, wmFD int, log zerowrap.Logger) (*XWM, error) { | |
| 153 | + | // The watcher shuts the socket down when ctx ends, which wakes any blocked | |
| 154 | + | // handshake, Sync or InternAtoms. It holds its own duplicate so a shutdown | |
| 155 | + | // can never reach a descriptor number reused after Boot closed wmFD. | |
| 156 | + | var watch *os.File | |
| 157 | + | if dup, err := unix.FcntlInt(uintptr(wmFD), unix.F_DUPFD_CLOEXEC, 0); err == nil { | |
| 158 | + | watch = os.NewFile(uintptr(dup), "xwm abort") | |
| 159 | + | } | |
| 160 | + | stopWatch := func() {} | |
| 161 | + | if watch != nil { | |
| 162 | + | quit, joined := make(chan struct{}), make(chan struct{}) | |
| 163 | + | go func() { | |
| 164 | + | defer close(joined) | |
| 165 | + | select { | |
| 166 | + | case <-ctx.Done(): | |
| 167 | + | if rc, err := watch.SyscallConn(); err == nil { | |
| 168 | + | _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) }) | |
| 169 | + | } | |
| 170 | + | case <-quit: | |
| 171 | + | } | |
| 172 | + | _ = watch.Close() | |
| 173 | + | }() | |
| 174 | + | stopWatch = func() { close(quit); <-joined } | |
| 175 | + | } | |
| 176 | + | x, err := boot(wmFD, log) | |
| 177 | + | stopWatch() | |
| 178 | + | if err == nil && ctx.Err() != nil { | |
| 179 | + | x.CloseConn() | |
| 180 | + | x, err = nil, ctx.Err() | |
| 181 | + | } | |
| 182 | + | if err != nil && ctx.Err() != nil { | |
| 183 | + | err = fmt.Errorf("%w (%w)", err, ctx.Err()) | |
| 184 | + | } | |
| 185 | + | return x, err | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | func boot(wmFD int, log zerowrap.Logger) (*XWM, error) { | |
| 149 | 189 | conn, err := xturbo.NewConn(wmFD) | |
| 150 | 190 | if err != nil { | |
| 151 | 191 | return nil, fmt.Errorf("xwm: connect: %w", err) | |
| @@ -8,10 +8,12 @@ | |||||
| 8 | package xwm | 8 | package xwm | ||
| 9 | 9 | ||||
| 10 | import ( | 10 | import ( | ||
| 11 | + | "context" | |||
| 11 | "encoding/binary" | 12 | "encoding/binary" | ||
| 12 | "errors" | 13 | "errors" | ||
| 13 | "fmt" | 14 | "fmt" | ||
| 14 | "io" | 15 | "io" | ||
| 16 | + | "os" | |||
| 15 | 17 | ||||
| 16 | "bnema.dev/xturbo" | 18 | "bnema.dev/xturbo" | ||
| 17 | "bnema.dev/xturbo/composite" | 19 | "bnema.dev/xturbo/composite" | ||
| @@ -20,6 +22,7 @@ import ( | |||||
| 20 | "bnema.dev/xturbo/xproto" | 22 | "bnema.dev/xturbo/xproto" | ||
| 21 | "github.com/bnema/purego-libwayland/server" | 23 | "github.com/bnema/purego-libwayland/server" | ||
| 22 | "github.com/bnema/zerowrap" | 24 | "github.com/bnema/zerowrap" | ||
| 25 | + | "golang.org/x/sys/unix" | |||
| 23 | ) | 26 | ) | ||
| 24 | 27 | ||||
| 25 | var ( | 28 | var ( | ||
| @@ -144,8 +147,45 @@ func (x *XWM) setProp32(win uint32, prop int, typ xproto.Atom, vals []uint32) { | |||||
| 144 | // blocks (about 25 s at worst) and must not run on the display goroutine: the | 147 | // blocks (about 25 s at worst) and must not run on the display goroutine: the | ||
| 145 | // handshake needs Xwayland, which needs the display goroutine to serve its | 148 | // handshake needs Xwayland, which needs the display goroutine to serve its | ||
| 146 | // Wayland requests. It takes ownership of wmFD and closes it on failure. | 149 | // Wayland requests. It takes ownership of wmFD and closes it on failure. | ||
| 147 | - | // log is scoped by the caller with logging.For(ctx, "xwm"). | 150 | + | // Boot gives up promptly when ctx ends (the Xwayland process died or the run |
| 148 | - | func Boot(wmFD int, log zerowrap.Logger) (*XWM, error) { | 151 | + | // was cancelled). log is scoped by the caller with logging.For(ctx, "xwm"). |
| 152 | + | func Boot(ctx context.Context, wmFD int, log zerowrap.Logger) (*XWM, error) { | |||
| 153 | + | // The watcher shuts the socket down when ctx ends, which wakes any blocked | |||
| 154 | + | // handshake, Sync or InternAtoms. It holds its own duplicate so a shutdown | |||
| 155 | + | // can never reach a descriptor number reused after Boot closed wmFD. | |||
| 156 | + | var watch *os.File | |||
| 157 | + | if dup, err := unix.FcntlInt(uintptr(wmFD), unix.F_DUPFD_CLOEXEC, 0); err == nil { | |||
| 158 | + | watch = os.NewFile(uintptr(dup), "xwm abort") | |||
| 159 | + | } | |||
| 160 | + | stopWatch := func() {} | |||
| 161 | + | if watch != nil { | |||
| 162 | + | quit, joined := make(chan struct{}), make(chan struct{}) | |||
| 163 | + | go func() { | |||
| 164 | + | defer close(joined) | |||
| 165 | + | select { | |||
| 166 | + | case <-ctx.Done(): | |||
| 167 | + | if rc, err := watch.SyscallConn(); err == nil { | |||
| 168 | + | _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) }) | |||
| 169 | + | } | |||
| 170 | + | case <-quit: | |||
| 171 | + | } | |||
| 172 | + | _ = watch.Close() | |||
| 173 | + | }() | |||
| 174 | + | stopWatch = func() { close(quit); <-joined } | |||
| 175 | + | } | |||
| 176 | + | x, err := boot(wmFD, log) | |||
| 177 | + | stopWatch() | |||
| 178 | + | if err == nil && ctx.Err() != nil { | |||
| 179 | + | x.CloseConn() | |||
| 180 | + | x, err = nil, ctx.Err() | |||
| 181 | + | } | |||
| 182 | + | if err != nil && ctx.Err() != nil { | |||
| 183 | + | err = fmt.Errorf("%w (%w)", err, ctx.Err()) | |||
| 184 | + | } | |||
| 185 | + | return x, err | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | func boot(wmFD int, log zerowrap.Logger) (*XWM, error) { | |||
| 149 | conn, err := xturbo.NewConn(wmFD) | 189 | conn, err := xturbo.NewConn(wmFD) | ||
| 150 | if err != nil { | 190 | if err != nil { | ||
| 151 | return nil, fmt.Errorf("xwm: connect: %w", err) | 191 | return nil, fmt.Errorf("xwm: connect: %w", err) | ||
M internal/adapters/wayland/xwm/xwm_test.go +6 -5
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package xwm | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "encoding/binary" | |
| 5 | 6 | "os" | |
| 6 | 7 | "strings" | |
| @@ -23,7 +24,7 @@ func bootPeer(t *testing.T, mutate func(p *xpeer)) (*XWM, *xpeer) { | |||
| 23 | 24 | if mutate != nil { | |
| 24 | 25 | p.set(mutate) | |
| 25 | 26 | } | |
| 26 | - | x, err := Boot(fd, testLog()) | |
| 27 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 27 | 28 | require.NoError(t, err) | |
| 28 | 29 | t.Cleanup(x.CloseConn) | |
| 29 | 30 | return x, p | |
| @@ -134,7 +135,7 @@ func TestBootRequests(t *testing.T) { | |||
| 134 | 135 | func TestBootWMExists(t *testing.T) { | |
| 135 | 136 | fd, p := startXPeer(t, fullPeerExts()) | |
| 136 | 137 | p.set(func(p *xpeer) { p.denyRoot = true }) | |
| 137 | - | x, err := Boot(fd, testLog()) | |
| 138 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 138 | 139 | require.ErrorIs(t, err, ErrWMExists) | |
| 139 | 140 | require.Nil(t, x) | |
| 140 | 141 | // Nothing the running window manager owns was touched. | |
| @@ -150,7 +151,7 @@ func TestBootNoComposite(t *testing.T) { | |||
| 150 | 151 | exts := fullPeerExts() | |
| 151 | 152 | delete(exts, "Composite") | |
| 152 | 153 | fd, _ := startXPeer(t, exts) | |
| 153 | - | _, err := Boot(fd, testLog()) | |
| 154 | + | _, err := Boot(context.Background(), fd, testLog()) | |
| 154 | 155 | require.ErrorIs(t, err, ErrNoComposite) | |
| 155 | 156 | } | |
| 156 | 157 | ||
| @@ -159,7 +160,7 @@ func TestBootOptionalExtensions(t *testing.T) { | |||
| 159 | 160 | delete(exts, "XFIXES") | |
| 160 | 161 | delete(exts, "X-Resource") | |
| 161 | 162 | fd, _ := startXPeer(t, exts) | |
| 162 | - | x, err := Boot(fd, testLog()) | |
| 163 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 163 | 164 | require.NoError(t, err) | |
| 164 | 165 | x.CloseConn() | |
| 165 | 166 | } | |
| @@ -167,7 +168,7 @@ func TestBootOptionalExtensions(t *testing.T) { | |||
| 167 | 168 | func TestBootConnectFailure(t *testing.T) { | |
| 168 | 169 | fd, p := startXPeer(t, fullPeerExts()) | |
| 169 | 170 | p.hangup() | |
| 170 | - | _, err := Boot(fd, testLog()) | |
| 171 | + | _, err := Boot(context.Background(), fd, testLog()) | |
| 171 | 172 | require.Error(t, err) | |
| 172 | 173 | } | |
| 173 | 174 | ||
| @@ -1,6 +1,7 @@ | |||||
| 1 | package xwm | 1 | package xwm | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "encoding/binary" | 5 | "encoding/binary" | ||
| 5 | "os" | 6 | "os" | ||
| 6 | "strings" | 7 | "strings" | ||
| @@ -23,7 +24,7 @@ func bootPeer(t *testing.T, mutate func(p *xpeer)) (*XWM, *xpeer) { | |||||
| 23 | if mutate != nil { | 24 | if mutate != nil { | ||
| 24 | p.set(mutate) | 25 | p.set(mutate) | ||
| 25 | } | 26 | } | ||
| 26 | - | x, err := Boot(fd, testLog()) | 27 | + | x, err := Boot(context.Background(), fd, testLog()) |
| 27 | require.NoError(t, err) | 28 | require.NoError(t, err) | ||
| 28 | t.Cleanup(x.CloseConn) | 29 | t.Cleanup(x.CloseConn) | ||
| 29 | return x, p | 30 | return x, p | ||
| @@ -134,7 +135,7 @@ func TestBootRequests(t *testing.T) { | |||||
| 134 | func TestBootWMExists(t *testing.T) { | 135 | func TestBootWMExists(t *testing.T) { | ||
| 135 | fd, p := startXPeer(t, fullPeerExts()) | 136 | fd, p := startXPeer(t, fullPeerExts()) | ||
| 136 | p.set(func(p *xpeer) { p.denyRoot = true }) | 137 | p.set(func(p *xpeer) { p.denyRoot = true }) | ||
| 137 | - | x, err := Boot(fd, testLog()) | 138 | + | x, err := Boot(context.Background(), fd, testLog()) |
| 138 | require.ErrorIs(t, err, ErrWMExists) | 139 | require.ErrorIs(t, err, ErrWMExists) | ||
| 139 | require.Nil(t, x) | 140 | require.Nil(t, x) | ||
| 140 | // Nothing the running window manager owns was touched. | 141 | // Nothing the running window manager owns was touched. | ||
| @@ -150,7 +151,7 @@ func TestBootNoComposite(t *testing.T) { | |||||
| 150 | exts := fullPeerExts() | 151 | exts := fullPeerExts() | ||
| 151 | delete(exts, "Composite") | 152 | delete(exts, "Composite") | ||
| 152 | fd, _ := startXPeer(t, exts) | 153 | fd, _ := startXPeer(t, exts) | ||
| 153 | - | _, err := Boot(fd, testLog()) | 154 | + | _, err := Boot(context.Background(), fd, testLog()) |
| 154 | require.ErrorIs(t, err, ErrNoComposite) | 155 | require.ErrorIs(t, err, ErrNoComposite) | ||
| 155 | } | 156 | } | ||
| 156 | 157 | ||||
| @@ -159,7 +160,7 @@ func TestBootOptionalExtensions(t *testing.T) { | |||||
| 159 | delete(exts, "XFIXES") | 160 | delete(exts, "XFIXES") | ||
| 160 | delete(exts, "X-Resource") | 161 | delete(exts, "X-Resource") | ||
| 161 | fd, _ := startXPeer(t, exts) | 162 | fd, _ := startXPeer(t, exts) | ||
| 162 | - | x, err := Boot(fd, testLog()) | 163 | + | x, err := Boot(context.Background(), fd, testLog()) |
| 163 | require.NoError(t, err) | 164 | require.NoError(t, err) | ||
| 164 | x.CloseConn() | 165 | x.CloseConn() | ||
| 165 | } | 166 | } | ||
| @@ -167,7 +168,7 @@ func TestBootOptionalExtensions(t *testing.T) { | |||||
| 167 | func TestBootConnectFailure(t *testing.T) { | 168 | func TestBootConnectFailure(t *testing.T) { | ||
| 168 | fd, p := startXPeer(t, fullPeerExts()) | 169 | fd, p := startXPeer(t, fullPeerExts()) | ||
| 169 | p.hangup() | 170 | p.hangup() | ||
| 170 | - | _, err := Boot(fd, testLog()) | 171 | + | _, err := Boot(context.Background(), fd, testLog()) |
| 171 | require.Error(t, err) | 172 | require.Error(t, err) | ||
| 172 | } | 173 | } | ||
| 173 | 174 | ||||
M internal/adapters/xwayland/builtin.go +17 -2
| @@ -88,7 +88,8 @@ func (builtinStarter) start(ctx context.Context, d *Display, stderr *logWriter) | |||
| 88 | 88 | } | |
| 89 | 89 | files[wl].Close() | |
| 90 | 90 | done := make(chan error, 1) | |
| 91 | - | go func() { done <- cmd.Wait() }() | |
| 91 | + | procGone := make(chan struct{}) | |
| 92 | + | go func() { done <- cmd.Wait(); close(procGone) }() | |
| 92 | 93 | if err != nil { | |
| 93 | 94 | stopGroup(cmd, done) | |
| 94 | 95 | return err | |
| @@ -115,7 +116,21 @@ func (builtinStarter) start(ctx context.Context, d *Display, stderr *logWriter) | |||
| 115 | 116 | stopGroup(cmd, done) | |
| 116 | 117 | return err | |
| 117 | 118 | } | |
| 118 | - | stop := d.opts.AttachWM(wmFD) | |
| 119 | + | // The context ends with the process or the caller's context, so a | |
| 120 | + | // bootstrap against a dead or silent Xwayland aborts at once. | |
| 121 | + | wmCtx, cancel := context.WithCancel(ctx) | |
| 122 | + | watched := make(chan struct{}) | |
| 123 | + | go func() { | |
| 124 | + | defer close(watched) | |
| 125 | + | select { | |
| 126 | + | case <-procGone: | |
| 127 | + | cancel() | |
| 128 | + | case <-wmCtx.Done(): | |
| 129 | + | } | |
| 130 | + | }() | |
| 131 | + | stop := d.opts.AttachWM(wmCtx, wmFD) | |
| 132 | + | cancel() | |
| 133 | + | <-watched | |
| 119 | 134 | if stop != nil { | |
| 120 | 135 | // Runs before start returns so the restart loop never overlaps two XWMs. | |
| 121 | 136 | defer stop() | |
| @@ -88,7 +88,8 @@ func (builtinStarter) start(ctx context.Context, d *Display, stderr *logWriter) | |||||
| 88 | } | 88 | } | ||
| 89 | files[wl].Close() | 89 | files[wl].Close() | ||
| 90 | done := make(chan error, 1) | 90 | done := make(chan error, 1) | ||
| 91 | - | go func() { done <- cmd.Wait() }() | 91 | + | procGone := make(chan struct{}) |
| 92 | + | go func() { done <- cmd.Wait(); close(procGone) }() | |||
| 92 | if err != nil { | 93 | if err != nil { | ||
| 93 | stopGroup(cmd, done) | 94 | stopGroup(cmd, done) | ||
| 94 | return err | 95 | return err | ||
| @@ -115,7 +116,21 @@ func (builtinStarter) start(ctx context.Context, d *Display, stderr *logWriter) | |||||
| 115 | stopGroup(cmd, done) | 116 | stopGroup(cmd, done) | ||
| 116 | return err | 117 | return err | ||
| 117 | } | 118 | } | ||
| 118 | - | stop := d.opts.AttachWM(wmFD) | 119 | + | // The context ends with the process or the caller's context, so a |
| 120 | + | // bootstrap against a dead or silent Xwayland aborts at once. | |||
| 121 | + | wmCtx, cancel := context.WithCancel(ctx) | |||
| 122 | + | watched := make(chan struct{}) | |||
| 123 | + | go func() { | |||
| 124 | + | defer close(watched) | |||
| 125 | + | select { | |||
| 126 | + | case <-procGone: | |||
| 127 | + | cancel() | |||
| 128 | + | case <-wmCtx.Done(): | |||
| 129 | + | } | |||
| 130 | + | }() | |||
| 131 | + | stop := d.opts.AttachWM(wmCtx, wmFD) | |||
| 132 | + | cancel() | |||
| 133 | + | <-watched | |||
| 119 | if stop != nil { | 134 | if stop != nil { | ||
| 120 | // Runs before start returns so the restart loop never overlaps two XWMs. | 135 | // Runs before start returns so the restart loop never overlaps two XWMs. | ||
| 121 | defer stop() | 136 | defer stop() | ||
M internal/adapters/xwayland/xwayland.go +5 -4
| @@ -28,10 +28,11 @@ type Options struct { | |||
| 28 | 28 | // starts. | |
| 29 | 29 | Attach func(wlFD int) | |
| 30 | 30 | // AttachWM takes ownership of the window manager descriptor once the | |
| 31 | - | // backend is ready. It may block to bootstrap the X11 connection. The | |
| 32 | - | // returned stop (nil for none) is called before the run ends, on backend | |
| 33 | - | // exit or context cancellation. | |
| 34 | - | AttachWM func(wmFD int) (stop func()) | |
| 31 | + | // backend is ready. It may block to bootstrap the X11 connection; ctx ends | |
| 32 | + | // when the backend exits or the run is cancelled, and the bootstrap must | |
| 33 | + | // then give up. The returned stop (nil for none) is called before the run | |
| 34 | + | // ends, on backend exit or context cancellation. | |
| 35 | + | AttachWM func(ctx context.Context, wmFD int) (stop func()) | |
| 35 | 36 | // Binary is the selected backend executable. | |
| 36 | 37 | Binary string | |
| 37 | 38 | // Env is the backend environment; satellite requires WAYLAND_DISPLAY. | |
| @@ -28,10 +28,11 @@ type Options struct { | |||||
| 28 | // starts. | 28 | // starts. | ||
| 29 | Attach func(wlFD int) | 29 | Attach func(wlFD int) | ||
| 30 | // AttachWM takes ownership of the window manager descriptor once the | 30 | // AttachWM takes ownership of the window manager descriptor once the | ||
| 31 | - | // backend is ready. It may block to bootstrap the X11 connection. The | 31 | + | // backend is ready. It may block to bootstrap the X11 connection; ctx ends |
| 32 | - | // returned stop (nil for none) is called before the run ends, on backend | 32 | + | // when the backend exits or the run is cancelled, and the bootstrap must |
| 33 | - | // exit or context cancellation. | 33 | + | // then give up. The returned stop (nil for none) is called before the run |
| 34 | - | AttachWM func(wmFD int) (stop func()) | 34 | + | // ends, on backend exit or context cancellation. |
| 35 | + | AttachWM func(ctx context.Context, wmFD int) (stop func()) | |||
| 35 | // Binary is the selected backend executable. | 36 | // Binary is the selected backend executable. | ||
| 36 | Binary string | 37 | Binary string | ||
| 37 | // Env is the backend environment; satellite requires WAYLAND_DISPLAY. | 38 | // Env is the backend environment; satellite requires WAYLAND_DISPLAY. | ||
M internal/adapters/xwayland/xwayland_test.go +30 -5
| @@ -388,7 +388,7 @@ func readyScript(t *testing.T, dir string, ready bool, tail string) (bin, marker | |||
| 388 | 388 | return bin, marker | |
| 389 | 389 | } | |
| 390 | 390 | ||
| 391 | - | func runBuiltin(t *testing.T, ctx context.Context, ready bool, tail string, attachWM func(wmFD int)) (events []string) { | |
| 391 | + | func runBuiltin(t *testing.T, ctx context.Context, ready bool, tail string, attachWM func(ctx context.Context, wmFD int)) (events []string) { | |
| 392 | 392 | t.Helper() | |
| 393 | 393 | opts := testOptions(t, "") | |
| 394 | 394 | opts.Backend = ports.XwaylandBuiltin | |
| @@ -402,12 +402,12 @@ func runBuiltin(t *testing.T, ctx context.Context, ready bool, tail string, atta | |||
| 402 | 402 | events = append(events, "attach") | |
| 403 | 403 | unix.Close(wlFD) | |
| 404 | 404 | } | |
| 405 | - | opts.AttachWM = func(wmFD int) func() { | |
| 405 | + | opts.AttachWM = func(ctx context.Context, wmFD int) func() { | |
| 406 | 406 | if !exists() { | |
| 407 | 407 | t.Error("AttachWM ran before readiness") | |
| 408 | 408 | } | |
| 409 | 409 | events = append(events, "attachWM") | |
| 410 | - | attachWM(wmFD) | |
| 410 | + | attachWM(ctx, wmFD) | |
| 411 | 411 | return func() { events = append(events, "stop") } | |
| 412 | 412 | } | |
| 413 | 413 | d, err := Open(opts) | |
| @@ -429,7 +429,7 @@ func TestBuiltinAttachWMAfterReadiness(t *testing.T) { | |||
| 429 | 429 | t.Run(name, func(t *testing.T) { | |
| 430 | 430 | ctx, cancel := context.WithCancel(t.Context()) | |
| 431 | 431 | defer cancel() | |
| 432 | - | events := runBuiltin(t, ctx, true, tc.tail, func(wmFD int) { | |
| 432 | + | events := runBuiltin(t, ctx, true, tc.tail, func(_ context.Context, wmFD int) { | |
| 433 | 433 | if _, err := unix.FcntlInt(uintptr(wmFD), unix.F_GETFD, 0); err != nil { | |
| 434 | 434 | t.Errorf("wmFD not open: %v", err) | |
| 435 | 435 | } | |
| @@ -447,8 +447,33 @@ func TestBuiltinAttachWMAfterReadiness(t *testing.T) { | |||
| 447 | 447 | ||
| 448 | 448 | // A process that exits before reporting readiness never reaches AttachWM. | |
| 449 | 449 | func TestBuiltinNoAttachWMWithoutReadiness(t *testing.T) { | |
| 450 | - | events := runBuiltin(t, t.Context(), false, "exit 1", func(int) {}) | |
| 450 | + | events := runBuiltin(t, t.Context(), false, "exit 1", func(context.Context, int) {}) | |
| 451 | 451 | if got := strings.Join(events, ","); got != "attach,returned" { | |
| 452 | 452 | t.Fatalf("events %s", got) | |
| 453 | 453 | } | |
| 454 | 454 | } | |
| 455 | + | ||
| 456 | + | // The AttachWM context ends when the process dies, so a bootstrap against a | |
| 457 | + | // dead Xwayland gives up at once instead of waiting out its handshake. | |
| 458 | + | func TestBuiltinAttachWMContextEndsWithProcess(t *testing.T) { | |
| 459 | + | ended := make(chan error, 1) | |
| 460 | + | start := time.Now() | |
| 461 | + | events := runBuiltin(t, t.Context(), true, "sleep 0.3", func(ctx context.Context, wmFD int) { | |
| 462 | + | defer unix.Close(wmFD) | |
| 463 | + | select { | |
| 464 | + | case <-ctx.Done(): | |
| 465 | + | ended <- ctx.Err() | |
| 466 | + | case <-time.After(5 * time.Second): | |
| 467 | + | ended <- nil | |
| 468 | + | } | |
| 469 | + | }) | |
| 470 | + | if err := <-ended; err == nil { | |
| 471 | + | t.Fatal("AttachWM context did not end with the process") | |
| 472 | + | } | |
| 473 | + | if time.Since(start) > 3*time.Second { | |
| 474 | + | t.Fatalf("took %v", time.Since(start)) | |
| 475 | + | } | |
| 476 | + | if got := strings.Join(events, ","); got != "attach,attachWM,stop,returned" { | |
| 477 | + | t.Fatalf("events %s", got) | |
| 478 | + | } | |
| 479 | + | } | |
| @@ -388,7 +388,7 @@ func readyScript(t *testing.T, dir string, ready bool, tail string) (bin, marker | |||||
| 388 | return bin, marker | 388 | return bin, marker | ||
| 389 | } | 389 | } | ||
| 390 | 390 | ||||
| 391 | - | func runBuiltin(t *testing.T, ctx context.Context, ready bool, tail string, attachWM func(wmFD int)) (events []string) { | 391 | + | func runBuiltin(t *testing.T, ctx context.Context, ready bool, tail string, attachWM func(ctx context.Context, wmFD int)) (events []string) { |
| 392 | t.Helper() | 392 | t.Helper() | ||
| 393 | opts := testOptions(t, "") | 393 | opts := testOptions(t, "") | ||
| 394 | opts.Backend = ports.XwaylandBuiltin | 394 | opts.Backend = ports.XwaylandBuiltin | ||
| @@ -402,12 +402,12 @@ func runBuiltin(t *testing.T, ctx context.Context, ready bool, tail string, atta | |||||
| 402 | events = append(events, "attach") | 402 | events = append(events, "attach") | ||
| 403 | unix.Close(wlFD) | 403 | unix.Close(wlFD) | ||
| 404 | } | 404 | } | ||
| 405 | - | opts.AttachWM = func(wmFD int) func() { | 405 | + | opts.AttachWM = func(ctx context.Context, wmFD int) func() { |
| 406 | if !exists() { | 406 | if !exists() { | ||
| 407 | t.Error("AttachWM ran before readiness") | 407 | t.Error("AttachWM ran before readiness") | ||
| 408 | } | 408 | } | ||
| 409 | events = append(events, "attachWM") | 409 | events = append(events, "attachWM") | ||
| 410 | - | attachWM(wmFD) | 410 | + | attachWM(ctx, wmFD) |
| 411 | return func() { events = append(events, "stop") } | 411 | return func() { events = append(events, "stop") } | ||
| 412 | } | 412 | } | ||
| 413 | d, err := Open(opts) | 413 | d, err := Open(opts) | ||
| @@ -429,7 +429,7 @@ func TestBuiltinAttachWMAfterReadiness(t *testing.T) { | |||||
| 429 | t.Run(name, func(t *testing.T) { | 429 | t.Run(name, func(t *testing.T) { | ||
| 430 | ctx, cancel := context.WithCancel(t.Context()) | 430 | ctx, cancel := context.WithCancel(t.Context()) | ||
| 431 | defer cancel() | 431 | defer cancel() | ||
| 432 | - | events := runBuiltin(t, ctx, true, tc.tail, func(wmFD int) { | 432 | + | events := runBuiltin(t, ctx, true, tc.tail, func(_ context.Context, wmFD int) { |
| 433 | if _, err := unix.FcntlInt(uintptr(wmFD), unix.F_GETFD, 0); err != nil { | 433 | if _, err := unix.FcntlInt(uintptr(wmFD), unix.F_GETFD, 0); err != nil { | ||
| 434 | t.Errorf("wmFD not open: %v", err) | 434 | t.Errorf("wmFD not open: %v", err) | ||
| 435 | } | 435 | } | ||
| @@ -447,8 +447,33 @@ func TestBuiltinAttachWMAfterReadiness(t *testing.T) { | |||||
| 447 | 447 | ||||
| 448 | // A process that exits before reporting readiness never reaches AttachWM. | 448 | // A process that exits before reporting readiness never reaches AttachWM. | ||
| 449 | func TestBuiltinNoAttachWMWithoutReadiness(t *testing.T) { | 449 | func TestBuiltinNoAttachWMWithoutReadiness(t *testing.T) { | ||
| 450 | - | events := runBuiltin(t, t.Context(), false, "exit 1", func(int) {}) | 450 | + | events := runBuiltin(t, t.Context(), false, "exit 1", func(context.Context, int) {}) |
| 451 | if got := strings.Join(events, ","); got != "attach,returned" { | 451 | if got := strings.Join(events, ","); got != "attach,returned" { | ||
| 452 | t.Fatalf("events %s", got) | 452 | t.Fatalf("events %s", got) | ||
| 453 | } | 453 | } | ||
| 454 | } | 454 | } | ||
| 455 | + | ||||
| 456 | + | // The AttachWM context ends when the process dies, so a bootstrap against a | |||
| 457 | + | // dead Xwayland gives up at once instead of waiting out its handshake. | |||
| 458 | + | func TestBuiltinAttachWMContextEndsWithProcess(t *testing.T) { | |||
| 459 | + | ended := make(chan error, 1) | |||
| 460 | + | start := time.Now() | |||
| 461 | + | events := runBuiltin(t, t.Context(), true, "sleep 0.3", func(ctx context.Context, wmFD int) { | |||
| 462 | + | defer unix.Close(wmFD) | |||
| 463 | + | select { | |||
| 464 | + | case <-ctx.Done(): | |||
| 465 | + | ended <- ctx.Err() | |||
| 466 | + | case <-time.After(5 * time.Second): | |||
| 467 | + | ended <- nil | |||
| 468 | + | } | |||
| 469 | + | }) | |||
| 470 | + | if err := <-ended; err == nil { | |||
| 471 | + | t.Fatal("AttachWM context did not end with the process") | |||
| 472 | + | } | |||
| 473 | + | if time.Since(start) > 3*time.Second { | |||
| 474 | + | t.Fatalf("took %v", time.Since(start)) | |||
| 475 | + | } | |||
| 476 | + | if got := strings.Join(events, ","); got != "attach,attachWM,stop,returned" { | |||
| 477 | + | t.Fatalf("events %s", got) | |||
| 478 | + | } | |||
| 479 | + | } | |||
M internal/app/app.go +1 -1
| @@ -191,7 +191,7 @@ func logScenes(log zerowrap.Logger, set []ports.Scene) { | |||
| 191 | 191 | ||
| 192 | 192 | // openXwayland reserves an X11 display for the selected backend, or returns | |
| 193 | 193 | // nil when X11 is off or unavailable: the session runs without it. | |
| 194 | - | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger, attach func(int), attachWM func(int) func()) *xwayland.Display { | |
| 194 | + | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger, attach func(int), attachWM func(context.Context, int) func()) *xwayland.Display { | |
| 195 | 195 | cfg := opts.Config.Xwayland | |
| 196 | 196 | bin := cfg.Binary | |
| 197 | 197 | if cfg.Backend == ports.XwaylandOff || opts.NoXwayland { | |
| @@ -191,7 +191,7 @@ func logScenes(log zerowrap.Logger, set []ports.Scene) { | |||||
| 191 | 191 | ||||
| 192 | // openXwayland reserves an X11 display for the selected backend, or returns | 192 | // openXwayland reserves an X11 display for the selected backend, or returns | ||
| 193 | // nil when X11 is off or unavailable: the session runs without it. | 193 | // nil when X11 is off or unavailable: the session runs without it. | ||
| 194 | - | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger, attach func(int), attachWM func(int) func()) *xwayland.Display { | 194 | + | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger, attach func(int), attachWM func(context.Context, int) func()) *xwayland.Display { |
| 195 | cfg := opts.Config.Xwayland | 195 | cfg := opts.Config.Xwayland | ||
| 196 | bin := cfg.Binary | 196 | bin := cfg.Binary | ||
| 197 | if cfg.Backend == ports.XwaylandOff || opts.NoXwayland { | 197 | if cfg.Backend == ports.XwaylandOff || opts.NoXwayland { | ||