Merge 17529ce1e512f84fe75f8eb022e1118eabbe015f into 3c13b3d3cf1deae3f997dd5bb3e9e0a55049380f
74 files · +11381 -144
M .mockery.yml +20 -0
| @@ -182,3 +182,23 @@ packages: | |||
| 182 | 182 | dir: internal/adapters/screencast | |
| 183 | 183 | pkgname: screencast | |
| 184 | 184 | filename: pipewire_opener_mock_test.go | |
| 185 | + | ||
| 186 | + | github.com/bnema/neferwl/internal/adapters/xwayland: | |
| 187 | + | interfaces: | |
| 188 | + | starter: | |
| 189 | + | config: | |
| 190 | + | dir: internal/adapters/xwayland | |
| 191 | + | pkgname: xwayland | |
| 192 | + | filename: starter_mock_test.go | |
| 193 | + | github.com/bnema/neferwl/internal/adapters/wayland/xwm: | |
| 194 | + | interfaces: | |
| 195 | + | host: | |
| 196 | + | config: | |
| 197 | + | dir: internal/adapters/wayland/xwm | |
| 198 | + | pkgname: xwm | |
| 199 | + | filename: host_mock_test.go | |
| 200 | + | fdSource: | |
| 201 | + | config: | |
| 202 | + | dir: internal/adapters/wayland/xwm | |
| 203 | + | pkgname: xwm | |
| 204 | + | filename: fdsource_mock_test.go | |
M Makefile +9 -2
| @@ -1,4 +1,4 @@ | |||
| 1 | - | .PHONY: build test vet race mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install | |
| 1 | + | .PHONY: build test vet race e2e-xwayland mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install | |
| 2 | 2 | ||
| 3 | 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | |
| 4 | 4 | TTY_TIMEOUT ?= 0 | |
| @@ -26,6 +26,7 @@ test: | |||
| 26 | 26 | CGO_ENABLED=0 go test ./... | |
| 27 | 27 | vet: | |
| 28 | 28 | CGO_ENABLED=0 go vet ./... | |
| 29 | + | CGO_ENABLED=0 go vet -tags xwayland ./internal/app | |
| 29 | 30 | race: | |
| 30 | 31 | # cgo is enabled only for the race test binary. | |
| 31 | 32 | CGO_ENABLED=1 go test -race ./... | |
| @@ -69,7 +70,8 @@ adapter-imports: | |||
| 69 | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 70 | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 71 | 72 | perf-check: | |
| 72 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=1 | |
| 73 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations)$$' -count=1 | |
| 74 | + | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 73 | 75 | check: vet test arch fakes-check log-check perf-check | |
| 74 | 76 | ||
| 75 | 77 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | |
| @@ -77,6 +79,11 @@ check: vet test arch fakes-check log-check perf-check | |||
| 77 | 79 | color-check: | |
| 78 | 80 | CGO_ENABLED=0 go test -tags colorcheck ./internal/app -run '^TestColorCheck' -count=1 -v | |
| 79 | 81 | ||
| 82 | + | # End-to-end test of the built-in XWM: a real Xwayland and an X11 probe on a | |
| 83 | + | # headless NeferWL. Needs Xwayland and a GPU; not part of check. | |
| 84 | + | e2e-xwayland: | |
| 85 | + | CGO_ENABLED=1 go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v | |
| 86 | + | ||
| 80 | 87 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | |
| 81 | 88 | # come from the module proxy in prepare(); the build itself runs offline. | |
| 82 | 89 | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | |
M README.md +1 -1
| @@ -149,7 +149,7 @@ See [Configuration](docs/config.md) for all keys, actions, bindings and automati | |||
| 149 | 149 | - Explicit sync carries client fences to the GPU and KMS, enabled by default with no flag. HDR10 on capable displays includes configurable SDR brightness. | |
| 150 | 150 | - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects. | |
| 151 | 151 | - Hidden apps get frame callbacks once per second and the suspended state, whether on another workspace, in a hidden stash, off screen or under fullscreen. Captured windows keep their output's refresh rate. | |
| 152 | - | - Steam and other X11 clients run through xwayland-satellite; Wine can run natively on Wayland. | |
| 152 | + | - Steam and other X11 clients run through xwayland-satellite, or through the experimental built-in window manager (`xwayland = builtin`; no menus, clipboard or XSETTINGS yet). Wine can run natively on Wayland. | |
| 153 | 153 | ||
| 154 | 154 | ### Windows and workspaces | |
| 155 | 155 | ||
M docs/config.md +3 -1
| @@ -27,7 +27,7 @@ See [examples/config](../examples/config) for every key with its default. | |||
| 27 | 27 | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | |
| 28 | 28 | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | |
| 29 | 29 | | `startup` | none | Commands run at session start, separated by `;` | | |
| 30 | - | | `xwayland` | `xwayland-satellite` | X11 support; `off` disables it | | |
| 30 | + | | `xwayland` | `satellite` | `off`, `satellite`, `builtin` or a satellite binary path | | |
| 31 | 31 | | `background` | `#111111` | Background color | | |
| 32 | 32 | | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps | | |
| 33 | 33 | | `border.width` | `2` | Border width in pixels; `0` hides borders | | |
| @@ -103,6 +103,8 @@ destination output, even if that output has no windows. Same-screen workspace | |||
| 103 | 103 | switches and mouse focus do not trigger a flash. | |
| 104 | 104 | It runs when `animations = on`. | |
| 105 | 105 | ||
| 106 | + | `xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus and popups (override-redirect windows), clipboard and XSETTINGS are not supported yet; use `satellite` if you need them. | |
| 107 | + | ||
| 106 | 108 | ## Automatic terminal | |
| 107 | 109 | ||
| 108 | 110 | The command comes from `terminal`, then `$TERMINAL`, then `foot`. | |
M docs/desktop.md +3 -1
| @@ -52,12 +52,14 @@ Candidate popups are not drawn yet. | |||
| 52 | 52 | ||
| 53 | 53 | ## X11 apps | |
| 54 | 54 | ||
| 55 | - | X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. | |
| 55 | + | By default, X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. | |
| 56 | 56 | ||
| 57 | 57 | ```text | |
| 58 | 58 | xwayland = off # no X11 support | |
| 59 | 59 | ``` | |
| 60 | 60 | ||
| 61 | + | With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus and popups, clipboard and XSETTINGS are not supported yet; use `satellite` if you need them. | |
| 62 | + | ||
| 61 | 63 | ## Session locking | |
| 62 | 64 | ||
| 63 | 65 | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input. | |
M docs/dev/headless.md +13 -0
| @@ -62,6 +62,19 @@ WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本 | |||
| 62 | 62 | ||
| 63 | 63 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | |
| 64 | 64 | ||
| 65 | + | ## X11 check | |
| 66 | + | ||
| 67 | + | `make e2e-xwayland` runs the end-to-end test of the built-in window manager (`xwayland = builtin`). It is behind the `xwayland` build tag, so `go test ./...` and `make check` only compile it (`go vet -tags xwayland`). It needs `Xwayland` on `PATH` and a GPU, and skips without Xwayland. | |
| 68 | + | ||
| 69 | + | The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. It checks: | |
| 70 | + | ||
| 71 | + | - the state lists one visible `X11Probe` window with the probe's pid, and the screenshot is mostly green inside its tile; | |
| 72 | + | - the probe receives focus; | |
| 73 | + | - a second probe tiles beside the first; | |
| 74 | + | - `key Super+q` closes the focused probe within 2 seconds and the state drops to one window. | |
| 75 | + | ||
| 76 | + | Run it by hand with `go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v`. | |
| 77 | + | ||
| 65 | 78 | ## Colour check | |
| 66 | 79 | ||
| 67 | 80 | `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`. | |
M go.mod +4 -3
| @@ -3,10 +3,11 @@ module github.com/bnema/neferwl | |||
| 3 | 3 | go 1.27 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | + | bnema.dev/xturbo v0.1.0 | |
| 6 | 7 | github.com/bnema/go-wayland-bindings v0.1.0 | |
| 7 | 8 | github.com/bnema/kvconf v0.1.2 | |
| 8 | - | github.com/bnema/purego v0.13.0-bnema.1 | |
| 9 | - | github.com/bnema/purego-libwayland v0.11.0 | |
| 9 | + | github.com/bnema/purego v0.13.0-bnema.2 | |
| 10 | + | github.com/bnema/purego-libwayland v0.12.0 | |
| 10 | 11 | github.com/bnema/purego-pipewire v0.2.0 | |
| 11 | 12 | github.com/bnema/purego-vulkan v0.6.0 | |
| 12 | 13 | github.com/bnema/wlturbo v0.5.0 | |
| @@ -16,7 +17,7 @@ require ( | |||
| 16 | 17 | github.com/rs/zerolog v1.35.1 | |
| 17 | 18 | github.com/stretchr/testify v1.12.1 | |
| 18 | 19 | golang.org/x/image v0.46.0 | |
| 19 | - | golang.org/x/sys v0.48.0 | |
| 20 | + | golang.org/x/sys v0.49.0 | |
| 20 | 21 | golang.org/x/term v0.46.0 | |
| 21 | 22 | ) | |
| 22 | 23 | ||
M go.sum +8 -6 generated file: collapsed
M internal/adapters/config/config.go +8 -4
| @@ -167,7 +167,7 @@ func Defaults() ports.Config { | |||
| 167 | 167 | // Hint mode is experimental: off unless the config turns it on. | |
| 168 | 168 | c.Hint.Enable = false | |
| 169 | 169 | c.Hint.TextClick = ports.ModCtrl | ports.ModShift | |
| 170 | - | c.Xwayland = "xwayland-satellite" | |
| 170 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"} | |
| 171 | 171 | c.Background.Color = "#111111" | |
| 172 | 172 | c.Floating.Dim = 0.3 | |
| 173 | 173 | c.Stash.Width = 80 | |
| @@ -726,11 +726,15 @@ func set(c *ports.Config, key, v string) error { | |||
| 726 | 726 | case "xwayland": | |
| 727 | 727 | switch { | |
| 728 | 728 | case v == "off": | |
| 729 | - | c.Xwayland = "" | |
| 729 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandOff} | |
| 730 | + | case v == "satellite": | |
| 731 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"} | |
| 732 | + | case v == "builtin": | |
| 733 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"} | |
| 730 | 734 | case v == "" || strings.ContainsAny(v, " \t"): | |
| 731 | - | return fmt.Errorf("must be off or the xwayland-satellite program") | |
| 735 | + | return fmt.Errorf("must be off, satellite, builtin or a satellite program") | |
| 732 | 736 | default: | |
| 733 | - | c.Xwayland = v | |
| 737 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: v} | |
| 734 | 738 | } | |
| 735 | 739 | case "background": | |
| 736 | 740 | if !color.MatchString(v) { | |
M internal/adapters/config/config_test.go +16 -6
| @@ -340,14 +340,24 @@ func TestWorkspacePrevNextAction(t *testing.T) { | |||
| 340 | 340 | } | |
| 341 | 341 | ||
| 342 | 342 | func TestXwayland(t *testing.T) { | |
| 343 | - | if Defaults().Xwayland != "xwayland-satellite" { | |
| 343 | + | if Defaults().Xwayland != (ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"}) { | |
| 344 | 344 | t.Fatal(Defaults().Xwayland) | |
| 345 | 345 | } | |
| 346 | - | if c, w := parseString(t, "xwayland = off"); c.Xwayland != "" || len(w) != 0 { | |
| 347 | - | t.Fatal(c.Xwayland, w) | |
| 348 | - | } | |
| 349 | - | if c, w := parseString(t, "xwayland = /opt/xwls"); c.Xwayland != "/opt/xwls" || len(w) != 0 { | |
| 350 | - | t.Fatal(c.Xwayland, w) | |
| 346 | + | for _, tt := range []struct { | |
| 347 | + | value string | |
| 348 | + | want ports.XwaylandConfig | |
| 349 | + | }{ | |
| 350 | + | {"off", ports.XwaylandConfig{Backend: ports.XwaylandOff}}, | |
| 351 | + | {"satellite", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite"}}, | |
| 352 | + | {"builtin", ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"}}, | |
| 353 | + | {"/opt/xwls", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "/opt/xwls"}}, | |
| 354 | + | } { | |
| 355 | + | t.Run(tt.value, func(t *testing.T) { | |
| 356 | + | c, w := parseString(t, "xwayland = "+tt.value) | |
| 357 | + | if c.Xwayland != tt.want || len(w) != 0 { | |
| 358 | + | t.Fatal(c.Xwayland, w) | |
| 359 | + | } | |
| 360 | + | }) | |
| 351 | 361 | } | |
| 352 | 362 | } | |
| 353 | 363 | ||
M internal/adapters/vulkan/capture_test.go +3 -0
| @@ -358,6 +358,9 @@ func openFDs(t *testing.T) int { | |||
| 358 | 358 | // slot keeps its sync file for every lease. No mock stands in the measured | |
| 359 | 359 | // closure. | |
| 360 | 360 | func TestCaptureCycleAllocations(t *testing.T) { | |
| 361 | + | if raceEnabled { | |
| 362 | + | t.Skip("allocation budgets are not stable under the race runtime") | |
| 363 | + | } | |
| 361 | 364 | for _, tc := range []struct { | |
| 362 | 365 | name string | |
| 363 | 366 | new func(*testing.T) *Renderer | |
A internal/adapters/vulkan/norace_test.go +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | //go:build !race | |
| 2 | + | ||
| 3 | + | package vulkan | |
| 4 | + | ||
| 5 | + | const raceEnabled = false | |
A internal/adapters/vulkan/race_test.go +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | //go:build race | |
| 2 | + | ||
| 3 | + | package vulkan | |
| 4 | + | ||
| 5 | + | // raceEnabled: sync.Pool drops items at random under the race runtime, so the | |
| 6 | + | // allocation budgets of calls that go through purego cannot hold. perf-check | |
| 7 | + | // runs them without it. | |
| 8 | + | const raceEnabled = true | |
M internal/adapters/vulkan/scene_perf_test.go +6 -0
| @@ -16,6 +16,9 @@ import ( | |||
| 16 | 16 | // A new scene Seq with one of two windows moved is a partial redraw seeded by | |
| 17 | 17 | // sceneDelta and allocates nothing either. | |
| 18 | 18 | func TestRenderSteadyStateAllocations(t *testing.T) { | |
| 19 | + | if raceEnabled { | |
| 20 | + | t.Skip("allocation budgets are not stable under the race runtime") | |
| 21 | + | } | |
| 19 | 22 | for _, tc := range []struct { | |
| 20 | 23 | name string | |
| 21 | 24 | transform ports.BufferTransform | |
| @@ -145,6 +148,9 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||
| 145 | 148 | // keeps one alloc of headroom for drivers whose import path differs. | |
| 146 | 149 | // Skipped without a GPU or /dev/udmabuf. | |
| 147 | 150 | func TestRenderSteadyStateDMABufAllocations(t *testing.T) { | |
| 151 | + | if raceEnabled { | |
| 152 | + | t.Skip("allocation budgets are not stable under the race runtime") | |
| 153 | + | } | |
| 148 | 154 | r, err := New(80, 32) | |
| 149 | 155 | if err != nil { | |
| 150 | 156 | t.Skipf("Vulkan unavailable: %v", err) | |
M internal/adapters/wayland/activation.go +3 -3
| @@ -66,7 +66,7 @@ func (a activation) Activate(_ *xdgactivation.ActivationV1, token string, surf * | |||
| 66 | 66 | } | |
| 67 | 67 | var w *window | |
| 68 | 68 | if reason == "" { | |
| 69 | - | if w = s.windows[state.root().windowID()]; w == nil || w.toplevel == nil || !w.mapped { | |
| 69 | + | if w = s.windows[state.root().windowID()]; w == nil || !w.isToplevel() || !w.mapped { | |
| 70 | 70 | reason = "not_toplevel" | |
| 71 | 71 | } | |
| 72 | 72 | } | |
| @@ -116,8 +116,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) { | |||
| 116 | 116 | ||
| 117 | 117 | // focusClient is the client with the keyboard focus. | |
| 118 | 118 | func (s *Server) focusClient() server.Client { | |
| 119 | - | if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() { | |
| 120 | - | return w.xdg.resource.Client() | |
| 119 | + | if w := s.windows[s.seat.focused]; w != nil && w.alive() { | |
| 120 | + | return w.client() | |
| 121 | 121 | } | |
| 122 | 122 | if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() { | |
| 123 | 123 | return l.resource.Client() | |
M internal/adapters/wayland/constraints.go +14 -13
| @@ -61,11 +61,11 @@ func (h pointerWarp) WarpPointer(r *pointerwarp.WpPointerWarpV1, surf *wayland.S | |||
| 61 | 61 | return | |
| 62 | 62 | } | |
| 63 | 63 | w := s.windows[s.seat.pointerFocus] | |
| 64 | - | if w == nil || !w.mapped || w.xdg.resource.Client() != r.Client() || s.surfaces[surf.Resource] != w.xdg.surface { | |
| 64 | + | if w == nil || !w.mapped || w.client() != r.Client() || s.surfaces[surf.Resource] != w.surf() { | |
| 65 | 65 | return | |
| 66 | 66 | } | |
| 67 | 67 | // Surface-local to window-local (core's, from the geometry origin). | |
| 68 | - | g := w.xdg.geometry | |
| 68 | + | g := w.geometry() | |
| 69 | 69 | s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)}) | |
| 70 | 70 | } | |
| 71 | 71 | ||
| @@ -80,12 +80,12 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool { | |||
| 80 | 80 | if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | |
| 81 | 81 | return false | |
| 82 | 82 | } | |
| 83 | - | if !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 83 | + | if !w.mapped || !w.alive() { | |
| 84 | 84 | return false | |
| 85 | 85 | } | |
| 86 | 86 | sent := false | |
| 87 | 87 | usec := uint64(c.Time / time.Microsecond) | |
| 88 | - | for _, rel := range s.relatives[w.xdg.resource.Client()] { | |
| 88 | + | for _, rel := range s.relatives[w.client()] { | |
| 89 | 89 | if rel.Resource.Alive() { | |
| 90 | 90 | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) | |
| 91 | 91 | sent = true | |
| @@ -272,7 +272,7 @@ func (s *Server) updateConstraint() { | |||
| 272 | 272 | } | |
| 273 | 273 | var want *constraint | |
| 274 | 274 | if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused { | |
| 275 | - | if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) { | |
| 275 | + | if c := s.constraints[w.surf()]; c != nil && !c.defunct && (c.active || c.contains(w)) { | |
| 276 | 276 | want = c | |
| 277 | 277 | } | |
| 278 | 278 | } | |
| @@ -298,8 +298,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 298 | 298 | if s.protected() { | |
| 299 | 299 | return | |
| 300 | 300 | } | |
| 301 | - | x := c.surface.xdg | |
| 302 | - | if x == nil || x.window == nil { | |
| 301 | + | w := c.surface.toplevelWindow() | |
| 302 | + | if w == nil { | |
| 303 | 303 | return | |
| 304 | 304 | } | |
| 305 | 305 | mode := ports.ConstraintConfine | |
| @@ -308,8 +308,9 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 308 | 308 | } | |
| 309 | 309 | r := c.region | |
| 310 | 310 | if r.W > 0 && r.H > 0 { | |
| 311 | - | r.X -= x.geometry.X | |
| 312 | - | r.Y -= x.geometry.Y | |
| 311 | + | g := w.geometry() | |
| 312 | + | r.X -= g.X | |
| 313 | + | r.Y -= g.Y | |
| 313 | 314 | } | |
| 314 | 315 | // Core supports one confinement rect, so this bounds the intersection; | |
| 315 | 316 | // contains uses the exact input region to decide activation. | |
| @@ -333,17 +334,17 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 333 | 334 | r = box | |
| 334 | 335 | } | |
| 335 | 336 | } | |
| 336 | - | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 337 | + | s.emit(ports.PointerConstrained{ID: w.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 337 | 338 | } | |
| 338 | 339 | ||
| 339 | 340 | // contains reports whether the pointer is in the region: a constraint | |
| 340 | 341 | // activates only there, so it never warps the pointer. | |
| 341 | 342 | func (c *constraint) contains(w *window) bool { | |
| 342 | 343 | r := c.region | |
| 343 | - | x, y := w.surfacePoint(w.xdg.server.seat.pointerX, w.xdg.server.seat.pointerY) | |
| 344 | + | x, y := w.surfacePoint(c.surface.server.seat.pointerX, c.surface.server.seat.pointerY) | |
| 344 | 345 | all, rects := c.surface.effectiveInput() | |
| 345 | 346 | if !all { | |
| 346 | - | localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y) | |
| 347 | + | localX, localY := x-float64(w.geometry().X), y-float64(w.geometry().Y) | |
| 347 | 348 | inside := false | |
| 348 | 349 | for _, input := range rects { | |
| 349 | 350 | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | |
| @@ -363,5 +364,5 @@ func (c *constraint) contains(w *window) bool { | |||
| 363 | 364 | ||
| 364 | 365 | // locked reports whether the window's pointer is locked. | |
| 365 | 366 | func (s *Server) locked(w *window) bool { | |
| 366 | - | return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface | |
| 367 | + | return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.surf() | |
| 367 | 368 | } | |
M internal/adapters/wayland/decoration.go +1 -1
| @@ -80,7 +80,7 @@ func (d *decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1) | |||
| 80 | 80 | func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) { | |
| 81 | 81 | r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | |
| 82 | 82 | for _, w := range d.s.windows { | |
| 83 | - | if w.toplevel == d.toplevel && w.hasLast && w.toplevel.Resource.Alive() && w.xdg.resource.Resource.Alive() { | |
| 83 | + | if w.toplevel == d.toplevel && w.hasLast && w.toplevelAlive() && w.alive() { | |
| 84 | 84 | w.sendConfigure() | |
| 85 | 85 | return | |
| 86 | 86 | } | |
M internal/adapters/wayland/dialog.go +1 -1
| @@ -77,6 +77,6 @@ func (w *window) setModal(modal bool) { | |||
| 77 | 77 | } | |
| 78 | 78 | w.modal = modal | |
| 79 | 79 | if w.mapped { | |
| 80 | - | w.xdg.server.emit(ports.WindowModal{ID: w.id, Modal: modal}) | |
| 80 | + | w.srv().emit(ports.WindowModal{ID: w.id, Modal: modal}) | |
| 81 | 81 | } | |
| 82 | 82 | } | |
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 | |
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
| @@ -72,7 +72,7 @@ func (*extToplevelList) Destroy(*ext.ExtForeignToplevelListV1) {} | |||
| 72 | 72 | func (s *Server) mappedToplevels() []ports.WindowID { | |
| 73 | 73 | ids := make([]ports.WindowID, 0, len(s.windows)) | |
| 74 | 74 | for id, w := range s.windows { | |
| 75 | - | if w.toplevel != nil && w.mapped { | |
| 75 | + | if w.isToplevel() && w.mapped { | |
| 76 | 76 | ids = append(ids, id) | |
| 77 | 77 | } | |
| 78 | 78 | } | |
M internal/adapters/wayland/foreigntoplevel.go +6 -6
| @@ -44,7 +44,7 @@ func registerForeignToplevel(d *server.Display, s *Server) error { | |||
| 44 | 44 | r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) } | |
| 45 | 45 | ids := make([]ports.WindowID, 0, len(s.windows)) | |
| 46 | 46 | for id, w := range s.windows { | |
| 47 | - | if w.toplevel != nil && w.mapped { | |
| 47 | + | if w.isToplevel() && w.mapped { | |
| 48 | 48 | ids = append(ids, id) | |
| 49 | 49 | } | |
| 50 | 50 | } | |
| @@ -186,7 +186,7 @@ func toplevelState(w *window, version int32) []byte { | |||
| 186 | 186 | ||
| 187 | 187 | // toplevelChanged updates every manager after w mapped or changed. | |
| 188 | 188 | func (s *Server) toplevelChanged(w *window) { | |
| 189 | - | if w.toplevel == nil || !w.mapped { | |
| 189 | + | if !w.isToplevel() || !w.mapped { | |
| 190 | 190 | return | |
| 191 | 191 | } | |
| 192 | 192 | for _, m := range s.toplevelManagers { | |
| @@ -220,7 +220,7 @@ func (s *Server) refreshToplevels() { | |||
| 220 | 220 | for _, l := range s.extToplevelLists { | |
| 221 | 221 | for id, h := range l.handles { | |
| 222 | 222 | w := s.windows[id] | |
| 223 | - | if h.closed || w == nil || w.toplevel == nil || !w.mapped { | |
| 223 | + | if h.closed || w == nil || !w.isToplevel() || !w.mapped { | |
| 224 | 224 | l.closed(id) | |
| 225 | 225 | } | |
| 226 | 226 | } | |
| @@ -231,7 +231,7 @@ func (s *Server) refreshToplevels() { | |||
| 231 | 231 | for _, m := range s.toplevelManagers { | |
| 232 | 232 | for id, h := range m.handles { | |
| 233 | 233 | w := s.windows[id] | |
| 234 | - | if h.req.closed || w == nil || w.toplevel == nil || !w.mapped { | |
| 234 | + | if h.req.closed || w == nil || !w.isToplevel() || !w.mapped { | |
| 235 | 235 | m.closed(id) | |
| 236 | 236 | } | |
| 237 | 237 | } | |
| @@ -253,7 +253,7 @@ func (t *toplevelRequests) window() *window { | |||
| 253 | 253 | if t.s.protected() || t.closed { | |
| 254 | 254 | return nil | |
| 255 | 255 | } | |
| 256 | - | if w := t.s.windows[t.id]; w != nil && w.toplevel != nil && w.mapped && w.toplevel.Resource.Alive() { | |
| 256 | + | if w := t.s.windows[t.id]; w != nil && w.toplevelAlive() && w.mapped { | |
| 257 | 257 | return w | |
| 258 | 258 | } | |
| 259 | 259 | return nil | |
| @@ -274,7 +274,7 @@ func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.S | |||
| 274 | 274 | } | |
| 275 | 275 | func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) { | |
| 276 | 276 | if w := t.window(); w != nil { | |
| 277 | - | w.toplevel.SendClose() | |
| 277 | + | w.sendClose() | |
| 278 | 278 | } | |
| 279 | 279 | } | |
| 280 | 280 | func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) { | |
M internal/adapters/wayland/frames.go +8 -6
| @@ -183,7 +183,7 @@ func (s *Server) pacePass(flipped map[string]bool, reports []ports.OutputPresent | |||
| 183 | 183 | func toplevelRoot(surf *surface) *surface { | |
| 184 | 184 | surf = surf.root() | |
| 185 | 185 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | |
| 186 | - | surf = surf.xdg.window.popup.parent.xdg.surface.root() | |
| 186 | + | surf = surf.xdg.window.popup.parent.surf().root() | |
| 187 | 187 | } | |
| 188 | 188 | return surf | |
| 189 | 189 | } | |
| @@ -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: | |
M internal/adapters/wayland/globals.go +10 -1
| @@ -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 | |
| @@ -131,8 +132,16 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||
| 131 | 132 | state.identity = c.server.nextSurface | |
| 132 | 133 | state.wl = w | |
| 133 | 134 | c.server.surfaces[w.Resource] = state | |
| 135 | + | c.server.surfaceByIdentity[state.identity] = state | |
| 134 | 136 | state.sendScale() | |
| 135 | - | w.OnDestroy = func() { delete(c.server.surfaces, w.Resource); state.Destroy(w) } | |
| 137 | + | w.OnDestroy = func() { | |
| 138 | + | delete(c.server.surfaces, w.Resource) | |
| 139 | + | delete(c.server.surfaceByIdentity, state.identity) | |
| 140 | + | state.Destroy(w) | |
| 141 | + | if state.kind == roleX11 && c.server.xwm != nil { | |
| 142 | + | c.server.xwm.SurfaceGone(state.identity) | |
| 143 | + | } | |
| 144 | + | } | |
| 136 | 145 | } | |
| 137 | 146 | } | |
| 138 | 147 | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { | |
M internal/adapters/wayland/imagecopy.go +1 -1
| @@ -351,7 +351,7 @@ func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok | |||
| 351 | 351 | case srcWindow: | |
| 352 | 352 | // Core's state gives the size; before it, the last configure. | |
| 353 | 353 | w := c.s.windows[src.window] | |
| 354 | - | if w == nil || !w.mapped || w.toplevel == nil || w.gen != src.gen { | |
| 354 | + | if w == nil || !w.mapped || !w.isToplevel() || w.gen != src.gen { | |
| 355 | 355 | return nil, image.Rectangle{}, false, false | |
| 356 | 356 | } | |
| 357 | 357 | name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height} | |
M internal/adapters/wayland/popup.go +2 -2
| @@ -31,7 +31,7 @@ func (s *Server) newPopup(x *xdgSurface, r *xdgshell.Surface, id uint32, parentX | |||
| 31 | 31 | var parent *window | |
| 32 | 32 | if parentXDG != nil { | |
| 33 | 33 | for _, w := range s.windows { | |
| 34 | - | if w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource { | |
| 34 | + | if w.xdg != nil && w.xdg.resource != nil && w.xdg.resource.Resource == parentXDG.Resource { | |
| 35 | 35 | parent = w | |
| 36 | 36 | } | |
| 37 | 37 | } | |
| @@ -166,7 +166,7 @@ func (p *popup) configure(c ports.ConfigurePopup) { | |||
| 166 | 166 | func (s *Server) topGrab(client server.Client, parent *window) bool { | |
| 167 | 167 | var top *window | |
| 168 | 168 | for _, w := range s.windows { | |
| 169 | - | if w.popup != nil && w.popup.grab && !w.popup.done && w.xdg.resource.Client() == client && (top == nil || w.id > top.id) { | |
| 169 | + | if w.popup != nil && w.popup.grab && !w.popup.done && w.client() == client && (top == nil || w.id > top.id) { | |
| 170 | 170 | top = w | |
| 171 | 171 | } | |
| 172 | 172 | } | |
M internal/adapters/wayland/scale.go +3 -3
| @@ -21,12 +21,12 @@ func (s *Server) outputOfSurface(surf *surface) *output { | |||
| 21 | 21 | surf = surf.root() | |
| 22 | 22 | // Popups are on their parent's output. | |
| 23 | 23 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | |
| 24 | - | surf = surf.xdg.window.popup.parent.xdg.surface.root() | |
| 24 | + | surf = surf.xdg.window.popup.parent.surf().root() | |
| 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: | |
M internal/adapters/wayland/screencast.go +1 -1
| @@ -146,7 +146,7 @@ func (s *Server) openCast(cmd ports.CastCommand) { | |||
| 146 | 146 | gone := ports.CaptureReasonOutputGone | |
| 147 | 147 | if cmd.Window != 0 { | |
| 148 | 148 | gone = ports.CaptureReasonWindowGone | |
| 149 | - | if w := s.windows[cmd.Window]; w != nil && w.mapped && w.toplevel != nil { | |
| 149 | + | if w := s.windows[cmd.Window]; w != nil && w.mapped && w.isToplevel() { | |
| 150 | 150 | src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen} | |
| 151 | 151 | open.Window = cmd.Window | |
| 152 | 152 | } | |
M internal/adapters/wayland/seat.go +9 -8
| @@ -276,10 +276,10 @@ func (s *Server) setModifiers(c ports.SetModifiers) { | |||
| 276 | 276 | } | |
| 277 | 277 | ||
| 278 | 278 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |
| 279 | - | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 279 | + | if w == nil || !w.mapped || !w.alive() { | |
| 280 | 280 | return nil | |
| 281 | 281 | } | |
| 282 | - | return s.clientKeyboards(w.xdg.resource.Client()) | |
| 282 | + | return s.clientKeyboards(w.client()) | |
| 283 | 283 | } | |
| 284 | 284 | ||
| 285 | 285 | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | |
| @@ -301,7 +301,7 @@ func (s *Server) focusTarget(id ports.WindowID) (*wayland.Surface, []*wayland.Ke | |||
| 301 | 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | |
| 302 | 302 | } | |
| 303 | 303 | if w := s.windows[id]; w != nil && w.mapped { | |
| 304 | - | if surf := w.xdg.surfaceResource(); surf != nil { | |
| 304 | + | if surf := w.surfaceResource(); surf != nil { | |
| 305 | 305 | return surf, s.windowKeyboards(w) | |
| 306 | 306 | } | |
| 307 | 307 | } | |
| @@ -319,8 +319,8 @@ func (s *Server) pointerTarget(id ports.WindowID) (server.Client, []*wayland.Poi | |||
| 319 | 319 | if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) { | |
| 320 | 320 | return l.resource.Client(), s.clientPointers(l.resource.Client()), true | |
| 321 | 321 | } | |
| 322 | - | if w := s.windows[id]; w != nil && w.mapped && w.xdg.resource.Resource.Alive() { | |
| 323 | - | return w.xdg.resource.Client(), s.windowPointers(w), true | |
| 322 | + | if w := s.windows[id]; w != nil && w.mapped && w.alive() { | |
| 323 | + | return w.client(), s.windowPointers(w), true | |
| 324 | 324 | } | |
| 325 | 325 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | |
| 326 | 326 | return l.resource.Client(), s.layerPointers(l), true | |
| @@ -335,7 +335,7 @@ func (s *Server) pointerSurface(id ports.WindowID, x, y float64) (*wayland.Surfa | |||
| 335 | 335 | return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y | |
| 336 | 336 | } | |
| 337 | 337 | if w := s.windows[id]; w != nil { | |
| 338 | - | if surf := w.xdg.surfaceResource(); surf != nil && surf.Resource.Alive() { | |
| 338 | + | if surf := w.surfaceResource(); surf != nil && surf.Resource.Alive() { | |
| 339 | 339 | x, y := w.surfacePoint(x, y) | |
| 340 | 340 | return surf, s.windowPointers(w), x, y | |
| 341 | 341 | } | |
| @@ -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 | } | |
| @@ -543,10 +544,10 @@ func pointerFrame(p *wayland.Pointer) { | |||
| 543 | 544 | } | |
| 544 | 545 | } | |
| 545 | 546 | func (s *Server) windowPointers(w *window) []*wayland.Pointer { | |
| 546 | - | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 547 | + | if w == nil || !w.mapped || !w.alive() { | |
| 547 | 548 | return nil | |
| 548 | 549 | } | |
| 549 | - | return s.clientPointers(w.xdg.resource.Client()) | |
| 550 | + | return s.clientPointers(w.client()) | |
| 550 | 551 | } | |
| 551 | 552 | ||
| 552 | 553 | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | |
M internal/adapters/wayland/server.go +36 -16
| @@ -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,13 +161,18 @@ type Server struct { | |||
| 160 | 161 | securityStop int | |
| 161 | 162 | securityWG sync.WaitGroup | |
| 162 | 163 | securityLive atomic.Int32 | |
| 163 | - | display *server.Display | |
| 164 | - | env procEnv | |
| 165 | - | name string | |
| 166 | - | cleanup func() | |
| 167 | - | log zerowrap.Logger | |
| 168 | - | logDamage bool | |
| 169 | - | 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 | |
| 170 | 176 | // awaiting holds frame callbacks by output name, due at its next | |
| 171 | 177 | // frame (frameDue, or its page flip). | |
| 172 | 178 | awaiting map[string][]*wayland.Callback | |
| @@ -214,8 +220,11 @@ type Server struct { | |||
| 214 | 220 | // foreignExports are the live xdg-foreign handles (foreign.go). | |
| 215 | 221 | foreignExports map[string]*foreignExport | |
| 216 | 222 | surfaces map[*server.Resource]*surface | |
| 217 | - | buffers map[*server.Resource]clientBuffer | |
| 218 | - | dmabuf *dmabufGlobal | |
| 223 | + | // surfaceByIdentity finds a surface by its identity (xwayland_shell_v1 | |
| 224 | + | // pairing); kept with surfaces. | |
| 225 | + | surfaceByIdentity map[uint64]*surface | |
| 226 | + | buffers map[*server.Resource]clientBuffer | |
| 227 | + | dmabuf *dmabufGlobal | |
| 219 | 228 | // hdrOutputs records confirmed DRM modesets; absent outputs are SDR. | |
| 220 | 229 | hdrOutputs map[string]ports.OutputHDR | |
| 221 | 230 | colorID uint64 | |
| @@ -366,8 +375,10 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 366 | 375 | logDamage: opts.LogDamage, | |
| 367 | 376 | channels: ch, | |
| 368 | 377 | surfaces: make(map[*server.Resource]*surface), | |
| 378 | + | surfaceByIdentity: make(map[uint64]*surface), | |
| 369 | 379 | buffers: make(map[*server.Resource]clientBuffer), | |
| 370 | 380 | windows: make(map[ports.WindowID]*window), | |
| 381 | + | x11Slots: make(map[xwm.Slot]*window), | |
| 371 | 382 | layers: make(map[ports.WindowID]*layerSurface), | |
| 372 | 383 | nextWindow: 1, | |
| 373 | 384 | eventReady: make(chan struct{}, 1), | |
| @@ -498,6 +509,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 498 | 509 | if err = s.openPaceTimer(); err != nil { | |
| 499 | 510 | return abort(err) | |
| 500 | 511 | } | |
| 512 | + | d.SetGlobalFilter(s.globalVisible) | |
| 501 | 513 | if err = registerGlobals(d, opts, s); err != nil { | |
| 502 | 514 | return abort(err) | |
| 503 | 515 | } | |
| @@ -585,6 +597,12 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 585 | 597 | } | |
| 586 | 598 | s.castInventoryChanged() | |
| 587 | 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 | + | } | |
| 605 | + | s.forgetXwayland() | |
| 588 | 606 | s.stopSecurityContexts() | |
| 589 | 607 | wg.Wait() | |
| 590 | 608 | return err | |
| @@ -774,9 +792,11 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 774 | 792 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | |
| 775 | 793 | w.popup.dismiss() | |
| 776 | 794 | } | |
| 795 | + | case ports.PlaceWindow: | |
| 796 | + | s.x11Place(c) | |
| 777 | 797 | case ports.ConfigureWindow: | |
| 778 | 798 | w := s.windows[c.ID] | |
| 779 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() { | |
| 799 | + | if w == nil || !w.toplevelAlive() || !w.alive() { | |
| 780 | 800 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | |
| 781 | 801 | return | |
| 782 | 802 | } | |
| @@ -787,16 +807,16 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 787 | 807 | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible | |
| 788 | 808 | w.last, w.hasLast = c, true | |
| 789 | 809 | if visibilityChanged { | |
| 790 | - | s.relocateCallbacks(w.xdg.surface) | |
| 810 | + | s.relocateCallbacks(w.surf()) | |
| 791 | 811 | s.dropFeedbacks(time.Now()) | |
| 792 | 812 | s.wakePacer() // reconsider FIFO barriers at the new period | |
| 793 | 813 | } | |
| 794 | 814 | w.sendConfigure() | |
| 795 | 815 | s.toplevelChanged(w) | |
| 796 | - | if scanoutChanged && w.xdg.surface != nil { | |
| 797 | - | s.dmabuf.resendSurface(w.xdg.surface) | |
| 816 | + | if scanoutChanged && w.surf() != nil { | |
| 817 | + | s.dmabuf.resendSurface(w.surf()) | |
| 798 | 818 | } | |
| 799 | - | if surf := w.xdg.surface; surf != nil && c.Output != "" { | |
| 819 | + | if surf := w.surf(); surf != nil && c.Output != "" { | |
| 800 | 820 | surf.sendTreeScale() | |
| 801 | 821 | } | |
| 802 | 822 | if scanoutChanged { | |
| @@ -817,10 +837,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 817 | 837 | s.setShortcutsInhibit(c) | |
| 818 | 838 | case ports.CloseWindow: | |
| 819 | 839 | w := s.windows[c.ID] | |
| 820 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | |
| 840 | + | if w == nil || !w.toplevelAlive() { | |
| 821 | 841 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | |
| 822 | 842 | return | |
| 823 | 843 | } | |
| 824 | - | w.toplevel.SendClose() | |
| 844 | + | w.sendClose() | |
| 825 | 845 | } | |
| 826 | 846 | } | |
M internal/adapters/wayland/surface.go +15 -2
| @@ -28,6 +28,9 @@ const ( | |||
| 28 | 28 | roleSessionLock | |
| 29 | 29 | // roleDragIcon is the icon of a data drag (dnd.go). | |
| 30 | 30 | roleDragIcon | |
| 31 | + | // roleX11 is the surface of an X11 window paired through | |
| 32 | + | // xwayland_shell_v1. | |
| 33 | + | roleX11 | |
| 31 | 34 | ) | |
| 32 | 35 | ||
| 33 | 36 | type surface struct { | |
| @@ -47,8 +50,12 @@ type surface struct { | |||
| 47 | 50 | formatOpaque bool | |
| 48 | 51 | kind roleKind | |
| 49 | 52 | xdg *xdgSurface | |
| 50 | - | layer *layerSurface | |
| 51 | - | 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 | |
| 52 | 59 | // bufferCommitted is permanent, including commits later discarded by FIFO. | |
| 53 | 60 | bufferCommitted bool | |
| 54 | 61 | server *Server | |
| @@ -192,6 +199,9 @@ func (s *surface) windowID() ports.WindowID { | |||
| 192 | 199 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | |
| 193 | 200 | return s.xdg.window.id | |
| 194 | 201 | } | |
| 202 | + | if s.x11win != nil && s.x11win.mapped { | |
| 203 | + | return s.x11win.id | |
| 204 | + | } | |
| 195 | 205 | if s.layer != nil && s.layer.mapped { | |
| 196 | 206 | return s.layer.id | |
| 197 | 207 | } | |
| @@ -566,6 +576,9 @@ func (s *surface) applyCommit(u *update) { | |||
| 566 | 576 | if s.xdg != nil && s.xdg.window != nil { | |
| 567 | 577 | s.xdg.window.afterCommit() | |
| 568 | 578 | } | |
| 579 | + | if s.x11win != nil { | |
| 580 | + | s.x11win.afterCommit() | |
| 581 | + | } | |
| 569 | 582 | if s.lock != nil { | |
| 570 | 583 | // Role callbacks precede content construction; publish lock placement | |
| 571 | 584 | // only once validated current dimensions are available. | |
A internal/adapters/wayland/window.go +112 -0
| @@ -0,0 +1,112 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 5 | + | "github.com/bnema/neferwl/internal/ports" | |
| 6 | + | "github.com/bnema/purego-libwayland/server" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Role-independent accessors of a window. Code outside xdg.go goes through | |
| 10 | + | // them instead of dereferencing w.xdg, which is nil for windows of another | |
| 11 | + | // role. | |
| 12 | + | ||
| 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 | + | } | |
| 23 | + | ||
| 24 | + | // surf is the window's surface, nil when it has none. | |
| 25 | + | func (w *window) surf() *surface { | |
| 26 | + | switch { | |
| 27 | + | case w.xdg != nil: | |
| 28 | + | return w.xdg.surface | |
| 29 | + | case w.x11 != nil: | |
| 30 | + | return w.x11.surface | |
| 31 | + | } | |
| 32 | + | return nil | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // alive reports whether the window's role object is alive. | |
| 36 | + | func (w *window) alive() bool { | |
| 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 | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // client is the window's client; the zero Client when it has no role object. | |
| 47 | + | func (w *window) client() server.Client { | |
| 48 | + | switch { | |
| 49 | + | case w.xdg != nil: | |
| 50 | + | return w.xdg.resource.Client() | |
| 51 | + | case w.x11 != nil && w.x11.surface != nil: | |
| 52 | + | return w.x11.surface.wl.Client() | |
| 53 | + | } | |
| 54 | + | return server.Client{} | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // geometry is the committed window geometry; empty means the whole surface. | |
| 58 | + | func (w *window) geometry() ports.Rect { | |
| 59 | + | if w.xdg != nil { | |
| 60 | + | return w.xdg.geometry | |
| 61 | + | } | |
| 62 | + | return ports.Rect{} | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // sendConfigure sends w.last to the client. | |
| 66 | + | func (w *window) sendConfigure() { | |
| 67 | + | switch { | |
| 68 | + | case w.xdg != nil: | |
| 69 | + | w.sendXDGConfigure() | |
| 70 | + | case w.x11 != nil: | |
| 71 | + | w.x11.server.x11Configure(w) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // sendClose asks the client to close the window. | |
| 76 | + | func (w *window) sendClose() { | |
| 77 | + | switch { | |
| 78 | + | case w.toplevel != nil: | |
| 79 | + | w.toplevel.SendClose() | |
| 80 | + | case w.x11 != nil: | |
| 81 | + | w.x11.server.x11Close(w) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // toplevelAlive reports whether the window is a toplevel whose protocol | |
| 86 | + | // object is alive. | |
| 87 | + | func (w *window) toplevelAlive() bool { | |
| 88 | + | if w.x11 != nil { | |
| 89 | + | return w.alive() | |
| 90 | + | } | |
| 91 | + | return w.toplevel != nil && w.toplevel.Resource.Alive() | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // surfaceResource is the window's wl_surface, nil when it has none or it died. | |
| 95 | + | func (w *window) surfaceResource() *wayland.Surface { | |
| 96 | + | switch { | |
| 97 | + | case w.xdg != nil: | |
| 98 | + | return w.xdg.surfaceResource() | |
| 99 | + | case w.x11 != nil && w.alive(): | |
| 100 | + | return w.x11.surface.wl | |
| 101 | + | } | |
| 102 | + | return nil | |
| 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 | } | |
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 | + | } | |
A internal/adapters/wayland/x11window.go +307 -0
| @@ -0,0 +1,307 @@ | |||
| 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 | + | if x.surface != nil && x.surface != surf { | |
| 179 | + | // A new surface replaces the shown one: core sees a remap. | |
| 180 | + | w.unmap() | |
| 181 | + | } | |
| 182 | + | x.drop() | |
| 183 | + | x.surface, surf.x11win = surf, w | |
| 184 | + | surf.role = func(buffer bool) { | |
| 185 | + | switch { | |
| 186 | + | case surf.destroyed: | |
| 187 | + | w.unmap() | |
| 188 | + | if x.surface == surf { | |
| 189 | + | x.surface = nil | |
| 190 | + | } | |
| 191 | + | surf.x11win = nil | |
| 192 | + | case buffer: | |
| 193 | + | x.tryMap() | |
| 194 | + | case w.mapped: | |
| 195 | + | w.unmap() | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | // The buffer may have been committed before the pairing. | |
| 199 | + | x.tryMap() | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | func (h hostImpl) Dissociate(slot xwm.Slot, destroyed bool) { | |
| 203 | + | w := h.window(slot) | |
| 204 | + | if w == nil { | |
| 205 | + | return | |
| 206 | + | } | |
| 207 | + | w.unmap() | |
| 208 | + | w.x11.drop() | |
| 209 | + | w.x11.ready = false | |
| 210 | + | if destroyed { | |
| 211 | + | for _, c := range h.s.windows { | |
| 212 | + | if c.parent == w { | |
| 213 | + | c.parent = nil | |
| 214 | + | } | |
| 215 | + | } | |
| 216 | + | delete(h.s.windows, w.id) | |
| 217 | + | delete(h.s.x11Slots, slot) | |
| 218 | + | h.s.foreignToplevelGone(w) | |
| 219 | + | } | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | func (h hostImpl) Title(slot xwm.Slot, title string) { | |
| 223 | + | if w := h.window(slot); w != nil && w.title != title { | |
| 224 | + | w.title = title | |
| 225 | + | h.s.toplevelChanged(w) | |
| 226 | + | if w.mapped { | |
| 227 | + | h.s.castInventoryChanged() | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | func (h hostImpl) AppID(slot xwm.Slot, appID string) { | |
| 233 | + | w := h.window(slot) | |
| 234 | + | if w == nil || w.appID == appID { | |
| 235 | + | return | |
| 236 | + | } | |
| 237 | + | w.appID = appID | |
| 238 | + | if w.mapped { | |
| 239 | + | h.s.emit(ports.WindowAppID{ID: w.id, AppID: appID}) | |
| 240 | + | h.s.toplevelChanged(w) | |
| 241 | + | h.s.castInventoryChanged() | |
| 242 | + | } | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | func (h hostImpl) Parent(slot xwm.Slot, parent xwm.Slot) { | |
| 246 | + | if w := h.window(slot); w != nil { | |
| 247 | + | w.setParent(h.window(parent)) | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | func (h hostImpl) Fullscreen(slot xwm.Slot, on bool) { | |
| 252 | + | w := h.window(slot) | |
| 253 | + | if w == nil { | |
| 254 | + | return | |
| 255 | + | } | |
| 256 | + | w.x11.info.Fullscreen = on | |
| 257 | + | if w.mapped { | |
| 258 | + | h.s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: on}) | |
| 259 | + | } | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | // drop unlinks the window from its surface. | |
| 263 | + | func (x *x11Window) drop() { | |
| 264 | + | if surf := x.surface; surf != nil { | |
| 265 | + | surf.x11win, surf.role = nil, nil | |
| 266 | + | x.surface = nil | |
| 267 | + | } | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | // tryMap maps the window once the XWM knows it and its surface shows a buffer. | |
| 271 | + | func (x *x11Window) tryMap() { | |
| 272 | + | surf := x.surface | |
| 273 | + | if surf == nil { | |
| 274 | + | return | |
| 275 | + | } | |
| 276 | + | w := surf.x11win | |
| 277 | + | if w == nil || w.mapped || !x.ready || surf.destroyed || surf.current == nil { | |
| 278 | + | return | |
| 279 | + | } | |
| 280 | + | s := x.server | |
| 281 | + | w.mapped = true | |
| 282 | + | w.gen++ | |
| 283 | + | w.floating = x.info.Floating | |
| 284 | + | w.floatW, w.floatH = 0, 0 | |
| 285 | + | if w.floating { | |
| 286 | + | w.floatW, w.floatH = x.info.Width, x.info.Height | |
| 287 | + | } | |
| 288 | + | var parent ports.WindowID | |
| 289 | + | if w.parent != nil && w.parent.mapped { | |
| 290 | + | parent = w.parent.id | |
| 291 | + | } | |
| 292 | + | 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}) | |
| 293 | + | if x.info.Fullscreen { | |
| 294 | + | s.emit(ports.WindowFullscreenRequest{ID: w.id, Fullscreen: true}) | |
| 295 | + | } | |
| 296 | + | s.syncInhibitors() | |
| 297 | + | s.toplevelChanged(w) | |
| 298 | + | s.castInventoryChanged() | |
| 299 | + | 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") | |
| 300 | + | // Called from a commit, the content follows right after; called from the | |
| 301 | + | // pairing, the surface already shows a buffer and is published now. | |
| 302 | + | if surf.has { | |
| 303 | + | surf.resetInputEmission() | |
| 304 | + | surf.redraw() | |
| 305 | + | surf.emitInput() | |
| 306 | + | } | |
| 307 | + | } | |
A internal/adapters/wayland/x11window_test.go +353 -0
| @@ -0,0 +1,353 @@ | |||
| 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/pointerconstraints" | |
| 12 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 13 | + | "github.com/bnema/go-wayland-bindings/server/xdgshell" | |
| 14 | + | "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |
| 15 | + | "github.com/bnema/neferwl/internal/ports" | |
| 16 | + | "github.com/bnema/wlturbo" | |
| 17 | + | "golang.org/x/sys/unix" | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | var x11Serial = uint64(0x1234_0000_0042) | |
| 21 | + | ||
| 22 | + | // x11Rig is a server with an Xwayland client (a real wlturbo client on the | |
| 23 | + | // adopted descriptor), a scripted X peer on the WM descriptor and the XWM | |
| 24 | + | // between them. | |
| 25 | + | type x11Rig struct { | |
| 26 | + | t *testing.T | |
| 27 | + | s *Server | |
| 28 | + | events chan ports.ClientEvent | |
| 29 | + | commands chan ports.ClientCommand | |
| 30 | + | dir string | |
| 31 | + | xc *wlturbo.Display | |
| 32 | + | peer *x11Peer | |
| 33 | + | stop func() | |
| 34 | + | shell uint32 | |
| 35 | + | comp uint32 | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func newX11Rig(t *testing.T) *x11Rig { | |
| 39 | + | t.Helper() | |
| 40 | + | s, events, commands, dir := lifecycleServer(t) | |
| 41 | + | r := &x11Rig{t: t, s: s, events: events, commands: commands, dir: dir} | |
| 42 | + | wl := xwaylandPair(t) | |
| 43 | + | s.AttachXwayland(wl[0]) | |
| 44 | + | f := os.NewFile(uintptr(wl[1]), "xwayland client") | |
| 45 | + | wl[1] = -1 | |
| 46 | + | conn, err := net.FileConn(f) | |
| 47 | + | f.Close() | |
| 48 | + | if err != nil { | |
| 49 | + | t.Fatal(err) | |
| 50 | + | } | |
| 51 | + | if r.xc, err = wlturbo.ConnectFromConn(conn); err != nil { | |
| 52 | + | t.Fatal(err) | |
| 53 | + | } | |
| 54 | + | t.Cleanup(func() { _ = r.xc.Close(); forgetWireClient(r.xc) }) | |
| 55 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 56 | + | t.Fatal(err) | |
| 57 | + | } | |
| 58 | + | wm, peer := startX11Peer(t) | |
| 59 | + | r.peer = peer | |
| 60 | + | r.stop = s.AttachXWM(context.Background(), wm) | |
| 61 | + | t.Cleanup(func() { r.stop() }) | |
| 62 | + | r.shell = bindProtocol(t, r.xc, "xwayland_shell_v1") | |
| 63 | + | r.comp = bindProtocol(t, r.xc, "wl_compositor") | |
| 64 | + | return r | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // x11Window announces and maps X window win with the properties of an | |
| 68 | + | // ordinary client, and waits for the XWM to be ready for it. | |
| 69 | + | func (r *x11Rig) x11Window(win uint32) { | |
| 70 | + | r.t.Helper() | |
| 71 | + | p := r.peer | |
| 72 | + | str := uint32(31) // XA_STRING | |
| 73 | + | p.setProp(win, 67, str, 8, []byte("inst\x00Class\x00")) // WM_CLASS | |
| 74 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("net title")) | |
| 75 | + | p.setProp(win, p.atom("_NET_WM_PID"), 6, 32, x11le32(4242)) | |
| 76 | + | p.createNotify(win) | |
| 77 | + | skip := len(p.requests()) | |
| 78 | + | p.mapRequest(win) | |
| 79 | + | p.waitReq(skip, func(q x11Req) bool { return q.major == 8 && q.u32(4) == win }) // MapWindow | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // pair sends the serial from both sides and commits a buffer on the surface. | |
| 83 | + | // It returns the wl_surface id of the Xwayland client. | |
| 84 | + | func (r *x11Rig) pair(win uint32, serial uint64) uint32 { | |
| 85 | + | r.t.Helper() | |
| 86 | + | r.peer.clientMessage(win, "WL_SURFACE_SERIAL", uint32(serial), uint32(serial>>32)) | |
| 87 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |
| 88 | + | requestProtocol(r.t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 89 | + | registerProtocol(r.t, r.xc, surf) | |
| 90 | + | requestProtocol(r.t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 91 | + | registerProtocol(r.t, r.xc, xs) | |
| 92 | + | requestProtocol(r.t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(serial), uint32(serial>>32)) | |
| 93 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(r.t, r.xc), int32(0), int32(0)) | |
| 94 | + | requestProtocol(r.t, r.xc, surf, wayland.SurfaceRequestCommit) | |
| 95 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 96 | + | r.t.Fatal(err) | |
| 97 | + | } | |
| 98 | + | return surf | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func awaitEvent[T ports.ClientEvent](t *testing.T, events <-chan ports.ClientEvent) T { | |
| 102 | + | t.Helper() | |
| 103 | + | for deadline := time.After(3 * time.Second); ; { | |
| 104 | + | select { | |
| 105 | + | case ev := <-events: | |
| 106 | + | if v, ok := ev.(T); ok { | |
| 107 | + | return v | |
| 108 | + | } | |
| 109 | + | case <-deadline: | |
| 110 | + | var zero T | |
| 111 | + | t.Fatalf("no %T event", zero) | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | func TestXwaylandGlobalFilter(t *testing.T) { | |
| 117 | + | s, _, _, dir := lifecycleServer(t) | |
| 118 | + | other := protocolClient(t, s, dir) | |
| 119 | + | for _, iface := range []string{"xwayland_shell_v1", "zwp_xwayland_keyboard_grab_manager_v1"} { | |
| 120 | + | if _, ok := other.Registry().FindGlobal(iface); ok { | |
| 121 | + | t.Errorf("ordinary client sees %s", iface) | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | if _, ok := other.Registry().FindGlobal("xdg_wm_base"); !ok { | |
| 125 | + | t.Error("ordinary globals filtered out") | |
| 126 | + | } | |
| 127 | + | r := newX11Rig(t) | |
| 128 | + | if _, ok := r.xc.Registry().FindGlobal("xwayland_shell_v1"); !ok { | |
| 129 | + | t.Error("Xwayland client misses xwayland_shell_v1") | |
| 130 | + | } | |
| 131 | + | // The ordinary client of the same server still does not see it. | |
| 132 | + | late := protocolClient(t, r.s, r.dir) | |
| 133 | + | if _, ok := late.Registry().FindGlobal("xwayland_shell_v1"); ok { | |
| 134 | + | t.Error("ordinary client sees xwayland_shell_v1 while Xwayland is attached") | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func TestXwaylandShellRoleErrors(t *testing.T) { | |
| 139 | + | for _, scenario := range []string{"xdg role first", "serial twice", "zero serial"} { | |
| 140 | + | t.Run(scenario, func(t *testing.T) { | |
| 141 | + | r := newX11Rig(t) | |
| 142 | + | c := r.xc | |
| 143 | + | wmBase := bindProtocol(t, c, "xdg_wm_base") | |
| 144 | + | surf := c.AllocateID() | |
| 145 | + | requestProtocol(t, c, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 146 | + | registerProtocol(t, c, surf) | |
| 147 | + | xs := c.AllocateID() | |
| 148 | + | switch scenario { | |
| 149 | + | case "xdg role first": | |
| 150 | + | xdg := xs | |
| 151 | + | requestProtocol(t, c, wmBase, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf) | |
| 152 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, c.AllocateID(), surf) | |
| 153 | + | expectProtocolError(t, c, r.shell, uint32(xwaylandshell.XwaylandShellV1ErrorRole)) | |
| 154 | + | case "serial twice": | |
| 155 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 156 | + | registerProtocol(t, c, xs) | |
| 157 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(1), uint32(0)) | |
| 158 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(2), uint32(0)) | |
| 159 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorAlreadyAssociated)) | |
| 160 | + | case "zero serial": | |
| 161 | + | requestProtocol(t, c, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 162 | + | registerProtocol(t, c, xs) | |
| 163 | + | requestProtocol(t, c, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(0), uint32(0)) | |
| 164 | + | expectProtocolError(t, c, xs, uint32(xwaylandshell.XwaylandSurfaceV1ErrorInvalidSerial)) | |
| 165 | + | } | |
| 166 | + | }) | |
| 167 | + | } | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | // A window announced by the X server and a surface carrying the same serial | |
| 171 | + | // map as one neferwl window with the X11 info; core commands reach the X side. | |
| 172 | + | func TestX11WindowLifecycle(t *testing.T) { | |
| 173 | + | r := newX11Rig(t) | |
| 174 | + | win := uint32(x11Win) | |
| 175 | + | r.x11Window(win) | |
| 176 | + | surf := r.pair(win, x11Serial) | |
| 177 | + | ||
| 178 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 179 | + | if m.AppID != "Class" || m.PID != 4242 || !m.Positioned || m.Floating || m.Slot != "" { | |
| 180 | + | t.Fatalf("mapped %+v", m) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | // Title and app id changes. | |
| 184 | + | p := r.peer | |
| 185 | + | p.setProp(win, p.atom("_NET_WM_NAME"), p.atom("UTF8_STRING"), 8, []byte("renamed")) | |
| 186 | + | p.propertyNotify(win, p.atom("_NET_WM_NAME")) | |
| 187 | + | p.setProp(win, 67, 31, 8, []byte("inst\x00Other\x00")) | |
| 188 | + | p.propertyNotify(win, 67) | |
| 189 | + | if a := awaitEvent[ports.WindowAppID](t, r.events); a.ID != m.ID || a.AppID != "Other" { | |
| 190 | + | t.Fatalf("app id %+v", a) | |
| 191 | + | } | |
| 192 | + | var title string | |
| 193 | + | waitTrue(t, "title", func() bool { | |
| 194 | + | r.s.display.Do(func() { | |
| 195 | + | if w := r.s.windows[m.ID]; w != nil { | |
| 196 | + | title = w.title | |
| 197 | + | } | |
| 198 | + | }) | |
| 199 | + | return title == "renamed" | |
| 200 | + | }) | |
| 201 | + | ||
| 202 | + | // Fullscreen request from the client. | |
| 203 | + | p.clientMessage(win, "_NET_WM_STATE", 1, p.atom("_NET_WM_STATE_FULLSCREEN")) | |
| 204 | + | if f := awaitEvent[ports.WindowFullscreenRequest](t, r.events); f.ID != m.ID || !f.Fullscreen || f.External { | |
| 205 | + | t.Fatalf("fullscreen %+v", f) | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // Configure and Place reach the X server as ConfigureWindow. | |
| 209 | + | skip := len(p.requests()) | |
| 210 | + | r.commands <- ports.ConfigureWindow{ID: m.ID, Width: 640, Height: 360, Visible: true} | |
| 211 | + | cfg, skip := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |
| 212 | + | if w := binary.LittleEndian.Uint32(cfg.raw[20:]); w != 640 { | |
| 213 | + | t.Fatalf("configured width %d, want 640 (%x)", w, cfg.raw) | |
| 214 | + | } | |
| 215 | + | r.commands <- ports.PlaceWindow{ID: m.ID, X: 10, Y: 20} | |
| 216 | + | cfg, _ = p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpConfigureWindow && q.u32(4) == win }) | |
| 217 | + | if x, y := int32(binary.LittleEndian.Uint32(cfg.raw[12:])), int32(binary.LittleEndian.Uint32(cfg.raw[16:])); x != 10 || y != 20 { | |
| 218 | + | t.Fatalf("placed at %d,%d, want 10,20", x, y) | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | // Focus. | |
| 222 | + | skip = len(p.requests()) | |
| 223 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 224 | + | if f, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpSetInputFocus }); f.u32(4) != win { | |
| 225 | + | t.Fatalf("focus on %#x, want %#x", f.u32(4), win) | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | // Close: without WM_DELETE_WINDOW the client is killed. | |
| 229 | + | skip = len(p.requests()) | |
| 230 | + | r.commands <- ports.CloseWindow{ID: m.ID} | |
| 231 | + | if k, _ := p.waitReq(skip, func(q x11Req) bool { return q.major == x11OpKillClient }); k.u32(4) != win { | |
| 232 | + | t.Fatalf("killed %#x, want %#x", k.u32(4), win) | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // Destroying the surface unmaps the window. | |
| 236 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestDestroy) | |
| 237 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 238 | + | t.Fatal(err) | |
| 239 | + | } | |
| 240 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 241 | + | t.Fatalf("unmapped %+v", u) | |
| 242 | + | } | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | // The serial may arrive on the Wayland side before the X side knows it, and | |
| 246 | + | // an UnmapNotify unmaps; a remap pairs again. | |
| 247 | + | func TestX11WindowSurfaceFirstAndRemap(t *testing.T) { | |
| 248 | + | r := newX11Rig(t) | |
| 249 | + | win := uint32(x11Win) | |
| 250 | + | p := r.peer | |
| 251 | + | surf, xs := r.xc.AllocateID(), r.xc.AllocateID() | |
| 252 | + | requestProtocol(t, r.xc, r.comp, wayland.CompositorRequestCreateSurface, surf) | |
| 253 | + | registerProtocol(t, r.xc, surf) | |
| 254 | + | requestProtocol(t, r.xc, r.shell, xwaylandshell.XwaylandShellV1RequestGetXwaylandSurface, xs, surf) | |
| 255 | + | registerProtocol(t, r.xc, xs) | |
| 256 | + | requestProtocol(t, r.xc, xs, xwaylandshell.XwaylandSurfaceV1RequestSetSerial, uint32(x11Serial), uint32(x11Serial>>32)) | |
| 257 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestAttach, shmBuffer(t, r.xc), int32(0), int32(0)) | |
| 258 | + | requestProtocol(t, r.xc, surf, wayland.SurfaceRequestCommit) | |
| 259 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 260 | + | t.Fatal(err) | |
| 261 | + | } | |
| 262 | + | r.x11Window(win) | |
| 263 | + | p.clientMessage(win, "WL_SURFACE_SERIAL", uint32(x11Serial), uint32(x11Serial>>32)) | |
| 264 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 265 | + | if m.Remap { | |
| 266 | + | t.Fatalf("first map flagged as remap: %+v", m) | |
| 267 | + | } | |
| 268 | + | p.unmapNotify(win) | |
| 269 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 270 | + | t.Fatalf("unmapped %+v", u) | |
| 271 | + | } | |
| 272 | + | p.destroyNotify(win) | |
| 273 | + | waitTrue(t, "window forgotten", func() bool { | |
| 274 | + | gone := false | |
| 275 | + | r.s.display.Do(func() { gone = r.s.windows[m.ID] == nil }) | |
| 276 | + | return gone | |
| 277 | + | }) | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | // stop closes the XWM: its windows unmap, and the server keeps serving. | |
| 281 | + | func TestX11StopUnmapsWindows(t *testing.T) { | |
| 282 | + | r := newX11Rig(t) | |
| 283 | + | win := uint32(x11Win) | |
| 284 | + | r.x11Window(win) | |
| 285 | + | r.pair(win, x11Serial) | |
| 286 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 287 | + | r.stop() | |
| 288 | + | if u := awaitEvent[ports.WindowUnmapped](t, r.events); u.ID != m.ID { | |
| 289 | + | t.Fatalf("unmapped %+v", u) | |
| 290 | + | } | |
| 291 | + | r.s.display.Do(func() { | |
| 292 | + | if r.s.xwm != nil || r.s.windows[m.ID] != nil || len(r.s.x11Slots) != 0 { | |
| 293 | + | t.Error("XWM state left behind") | |
| 294 | + | } | |
| 295 | + | }) | |
| 296 | + | r.stop() // idempotent | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | // Boot gives up as soon as its context ends, even against a silent server, | |
| 300 | + | // and leaves the server serving. | |
| 301 | + | func TestAttachXWMBootAbortsWithContext(t *testing.T) { | |
| 302 | + | s, _, _, dir := lifecycleServer(t) | |
| 303 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 304 | + | if err != nil { | |
| 305 | + | t.Fatal(err) | |
| 306 | + | } | |
| 307 | + | defer unix.Close(fds[1]) // the silent server end | |
| 308 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 309 | + | done := make(chan func(), 1) | |
| 310 | + | go func() { done <- s.AttachXWM(ctx, fds[0]) }() | |
| 311 | + | select { | |
| 312 | + | case <-done: | |
| 313 | + | t.Fatal("AttachXWM returned before the context ended") | |
| 314 | + | case <-time.After(150 * time.Millisecond): | |
| 315 | + | } | |
| 316 | + | cancel() | |
| 317 | + | select { | |
| 318 | + | case stop := <-done: | |
| 319 | + | stop() | |
| 320 | + | case <-time.After(2 * time.Second): | |
| 321 | + | t.Fatal("AttachXWM did not abort with its context") | |
| 322 | + | } | |
| 323 | + | assertFDClosed(t, fds[0]) | |
| 324 | + | protocolClient(t, s, dir) // still serving | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | // Xwayland locks the pointer for games: the constraint reaches core for the | |
| 328 | + | // X11 window like for an xdg toplevel. | |
| 329 | + | func TestX11WindowPointerLock(t *testing.T) { | |
| 330 | + | r := newX11Rig(t) | |
| 331 | + | win := uint32(x11Win) | |
| 332 | + | r.x11Window(win) | |
| 333 | + | surf := r.pair(win, x11Serial) | |
| 334 | + | m := awaitEvent[ports.WindowMapped](t, r.events) | |
| 335 | + | ||
| 336 | + | seat := bindProtocol(t, r.xc, "wl_seat") | |
| 337 | + | registerProtocol(t, r.xc, seat) | |
| 338 | + | pointer := r.xc.AllocateID() | |
| 339 | + | requestProtocol(t, r.xc, seat, wayland.SeatRequestGetPointer, pointer) | |
| 340 | + | registerProtocol(t, r.xc, pointer) | |
| 341 | + | constraints := bindProtocol(t, r.xc, "zwp_pointer_constraints_v1") | |
| 342 | + | lock := r.xc.AllocateID() | |
| 343 | + | requestProtocol(t, r.xc, constraints, pointerconstraints.ZwpPointerConstraintsV1RequestLockPointer, lock, surf, pointer, uint32(0), uint32(pointerconstraints.ZwpPointerConstraintsV1LifetimePersistent)) | |
| 344 | + | registerProtocol(t, r.xc, lock) | |
| 345 | + | if err := r.xc.Roundtrip(); err != nil { | |
| 346 | + | t.Fatal(err) | |
| 347 | + | } | |
| 348 | + | r.commands <- ports.PointerFocus{ID: m.ID} | |
| 349 | + | r.commands <- ports.FocusWindow{ID: m.ID} | |
| 350 | + | if got := constrained(t, r.events); got.ID != m.ID || got.Mode != ports.ConstraintLock { | |
| 351 | + | t.Fatalf("constrained %+v", got) | |
| 352 | + | } | |
| 353 | + | } | |
M internal/adapters/wayland/xdg.go +32 -21
| @@ -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 | |
| @@ -151,8 +153,8 @@ type window struct { | |||
| 151 | 153 | hasLast bool | |
| 152 | 154 | } | |
| 153 | 155 | ||
| 154 | - | // sendConfigure sends w.last as xdg_toplevel + xdg_surface configure. | |
| 155 | - | func (w *window) sendConfigure() { | |
| 156 | + | // sendXDGConfigure sends w.last as xdg_toplevel + xdg_surface configure. | |
| 157 | + | func (w *window) sendXDGConfigure() { | |
| 156 | 158 | c := w.last | |
| 157 | 159 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version())) | |
| 158 | 160 | s := w.xdg.server | |
| @@ -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 | ||
A internal/adapters/wayland/xwayland.go +75 -0
| @@ -0,0 +1,75 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 7 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // xwaylandClient belongs to the display goroutine. | |
| 13 | + | type xwaylandClient struct { | |
| 14 | + | client server.Client | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // AttachXwayland transfers ownership of the Wayland descriptor to the display. | |
| 18 | + | // It may be called from any goroutine. | |
| 19 | + | func (s *Server) AttachXwayland(wlFD int) { | |
| 20 | + | ran := s.display.Do(func() { | |
| 21 | + | c, err := s.display.CreateClient(wlFD) | |
| 22 | + | if err != nil { | |
| 23 | + | s.log.Warn().Err(err).Msg("Xwayland connection not adopted") | |
| 24 | + | _ = unix.Close(wlFD) // CreateClient only adopts on success. | |
| 25 | + | return | |
| 26 | + | } | |
| 27 | + | s.forgetXwayland() | |
| 28 | + | s.xwayland = xwaylandClient{client: c} | |
| 29 | + | c.OnDestroy(func() { | |
| 30 | + | if s.xwayland.client == c { | |
| 31 | + | s.teardownXWM() | |
| 32 | + | s.forgetXwayland() | |
| 33 | + | } | |
| 34 | + | }) | |
| 35 | + | s.log.Info().Msg("Xwayland attached") | |
| 36 | + | }) | |
| 37 | + | if !ran { | |
| 38 | + | _ = unix.Close(wlFD) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | // forgetXwayland runs on the display owner, or after Run has joined it. | |
| 43 | + | func (s *Server) forgetXwayland() { | |
| 44 | + | if s.xwayland.client != (server.Client{}) { | |
| 45 | + | s.xwayland = xwaylandClient{} | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 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 | + | } | |
| 75 | + | } | |
A internal/adapters/wayland/xwayland_test.go +84 -0
| @@ -0,0 +1,84 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/purego-libwayland/server" | |
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func xwaylandPair(t *testing.T) *[2]int { | |
| 12 | + | t.Helper() | |
| 13 | + | p, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 14 | + | if err != nil { | |
| 15 | + | t.Fatal(err) | |
| 16 | + | } | |
| 17 | + | t.Cleanup(func() { | |
| 18 | + | if p[1] >= 0 { | |
| 19 | + | unix.Close(p[1]) | |
| 20 | + | } | |
| 21 | + | }) | |
| 22 | + | return &p | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | func assertFDClosed(t *testing.T, fd int) { | |
| 26 | + | t.Helper() | |
| 27 | + | if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); !errors.Is(err, unix.EBADF) { | |
| 28 | + | t.Fatalf("fd %d still open: %v", fd, err) | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func TestAttachXwaylandLifecycle(t *testing.T) { | |
| 33 | + | h := newAdmissionHarness(t, 1) | |
| 34 | + | wl := xwaylandPair(t) | |
| 35 | + | h.s.AttachXwayland(wl[0]) | |
| 36 | + | var first server.Client | |
| 37 | + | h.s.display.Do(func() { first = h.s.xwayland.client }) | |
| 38 | + | if first == (server.Client{}) { | |
| 39 | + | t.Fatal("client not recorded") | |
| 40 | + | } | |
| 41 | + | wl2 := xwaylandPair(t) | |
| 42 | + | h.s.AttachXwayland(wl2[0]) | |
| 43 | + | unix.Close(wl[1]) | |
| 44 | + | wl[1] = -1 // Old client's destroy must not clear the new record. | |
| 45 | + | waitTrue(t, "old client destroyed", func() bool { | |
| 46 | + | _, err := unix.FcntlInt(uintptr(wl[0]), unix.F_GETFD, 0) | |
| 47 | + | return errors.Is(err, unix.EBADF) | |
| 48 | + | }) | |
| 49 | + | h.s.display.Do(func() { | |
| 50 | + | if h.s.xwayland.client == (server.Client{}) || h.s.xwayland.client == first { | |
| 51 | + | t.Error("replacement lost") | |
| 52 | + | } | |
| 53 | + | }) | |
| 54 | + | unix.Close(wl2[1]) | |
| 55 | + | wl2[1] = -1 | |
| 56 | + | waitTrue(t, "Xwayland disconnected", func() bool { | |
| 57 | + | clear := false | |
| 58 | + | h.s.display.Do(func() { clear = h.s.xwayland.client == (server.Client{}) }) | |
| 59 | + | return clear | |
| 60 | + | }) | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func TestAttachXwaylandFailureClosesFDs(t *testing.T) { | |
| 64 | + | h := newAdmissionHarness(t, 1) | |
| 65 | + | var p [2]int | |
| 66 | + | if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil { | |
| 67 | + | t.Fatal(err) | |
| 68 | + | } | |
| 69 | + | defer unix.Close(p[1]) | |
| 70 | + | h.s.AttachXwayland(p[0]) | |
| 71 | + | assertFDClosed(t, p[0]) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func TestAttachXwaylandStopped(t *testing.T) { | |
| 75 | + | d, err := server.NewDisplay() | |
| 76 | + | if err != nil { | |
| 77 | + | t.Fatal(err) | |
| 78 | + | } | |
| 79 | + | d.Close() | |
| 80 | + | s := &Server{display: d} | |
| 81 | + | wl := xwaylandPair(t) | |
| 82 | + | s.AttachXwayland(wl[0]) | |
| 83 | + | assertFDClosed(t, wl[0]) | |
| 84 | + | } | |
A internal/adapters/wayland/xwm/atoms.go +96 -0
| @@ -0,0 +1,96 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | // Atoms interned at boot in one round trip, in this fixed order. The table | |
| 4 | + | // includes the clipboard and selection atoms. | |
| 5 | + | const ( | |
| 6 | + | atomWMProtocols = iota | |
| 7 | + | atomWMDeleteWindow | |
| 8 | + | atomWMTakeFocus | |
| 9 | + | atomWMState | |
| 10 | + | atomWMChangeState | |
| 11 | + | atomWMS0 | |
| 12 | + | atomUTF8String | |
| 13 | + | atomNetWMCMS0 | |
| 14 | + | atomNetSupported | |
| 15 | + | atomNetSupportingWMCheck | |
| 16 | + | atomNetWMName | |
| 17 | + | atomNetWMPID | |
| 18 | + | atomNetWMWindowType | |
| 19 | + | atomNetWMWindowTypeNormal | |
| 20 | + | atomNetWMWindowTypeDialog | |
| 21 | + | atomNetWMWindowTypeUtility | |
| 22 | + | atomNetWMWindowTypeSplash | |
| 23 | + | atomNetWMWindowTypeToolbar | |
| 24 | + | atomNetWMWindowTypeMenu | |
| 25 | + | atomNetWMWindowTypeDropdownMenu | |
| 26 | + | atomNetWMWindowTypePopupMenu | |
| 27 | + | atomNetWMWindowTypeTooltip | |
| 28 | + | atomNetWMWindowTypeNotification | |
| 29 | + | atomNetWMWindowTypeCombo | |
| 30 | + | atomNetWMWindowTypeDND | |
| 31 | + | atomNetWMState | |
| 32 | + | atomNetWMStateModal | |
| 33 | + | atomNetWMStateFullscreen | |
| 34 | + | atomNetWMStateFocused | |
| 35 | + | atomNetWMStateMaximizedVert | |
| 36 | + | atomNetWMStateMaximizedHorz | |
| 37 | + | atomNetActiveWindow | |
| 38 | + | atomNetClientList | |
| 39 | + | atomNetClientListStacking | |
| 40 | + | atomNetWMMoveresize | |
| 41 | + | atomNetWMBypassCompositor | |
| 42 | + | atomWLSurfaceID | |
| 43 | + | atomWLSurfaceSerial | |
| 44 | + | atomNetWMIcon | |
| 45 | + | atomMotifWMHints | |
| 46 | + | atomClipboard | |
| 47 | + | atomPrimary | |
| 48 | + | atomTargets | |
| 49 | + | atomCount | |
| 50 | + | ) | |
| 51 | + | ||
| 52 | + | var atomNames = [atomCount]string{ | |
| 53 | + | atomWMProtocols: "WM_PROTOCOLS", | |
| 54 | + | atomWMDeleteWindow: "WM_DELETE_WINDOW", | |
| 55 | + | atomWMTakeFocus: "WM_TAKE_FOCUS", | |
| 56 | + | atomWMState: "WM_STATE", | |
| 57 | + | atomWMChangeState: "WM_CHANGE_STATE", | |
| 58 | + | atomWMS0: "WM_S0", | |
| 59 | + | atomUTF8String: "UTF8_STRING", | |
| 60 | + | atomNetWMCMS0: "_NET_WM_CM_S0", | |
| 61 | + | atomNetSupported: "_NET_SUPPORTED", | |
| 62 | + | atomNetSupportingWMCheck: "_NET_SUPPORTING_WM_CHECK", | |
| 63 | + | atomNetWMName: "_NET_WM_NAME", | |
| 64 | + | atomNetWMPID: "_NET_WM_PID", | |
| 65 | + | atomNetWMWindowType: "_NET_WM_WINDOW_TYPE", | |
| 66 | + | atomNetWMWindowTypeNormal: "_NET_WM_WINDOW_TYPE_NORMAL", | |
| 67 | + | atomNetWMWindowTypeDialog: "_NET_WM_WINDOW_TYPE_DIALOG", | |
| 68 | + | atomNetWMWindowTypeUtility: "_NET_WM_WINDOW_TYPE_UTILITY", | |
| 69 | + | atomNetWMWindowTypeSplash: "_NET_WM_WINDOW_TYPE_SPLASH", | |
| 70 | + | atomNetWMWindowTypeToolbar: "_NET_WM_WINDOW_TYPE_TOOLBAR", | |
| 71 | + | atomNetWMWindowTypeMenu: "_NET_WM_WINDOW_TYPE_MENU", | |
| 72 | + | atomNetWMWindowTypeDropdownMenu: "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", | |
| 73 | + | atomNetWMWindowTypePopupMenu: "_NET_WM_WINDOW_TYPE_POPUP_MENU", | |
| 74 | + | atomNetWMWindowTypeTooltip: "_NET_WM_WINDOW_TYPE_TOOLTIP", | |
| 75 | + | atomNetWMWindowTypeNotification: "_NET_WM_WINDOW_TYPE_NOTIFICATION", | |
| 76 | + | atomNetWMWindowTypeCombo: "_NET_WM_WINDOW_TYPE_COMBO", | |
| 77 | + | atomNetWMWindowTypeDND: "_NET_WM_WINDOW_TYPE_DND", | |
| 78 | + | atomNetWMState: "_NET_WM_STATE", | |
| 79 | + | atomNetWMStateModal: "_NET_WM_STATE_MODAL", | |
| 80 | + | atomNetWMStateFullscreen: "_NET_WM_STATE_FULLSCREEN", | |
| 81 | + | atomNetWMStateFocused: "_NET_WM_STATE_FOCUSED", | |
| 82 | + | atomNetWMStateMaximizedVert: "_NET_WM_STATE_MAXIMIZED_VERT", | |
| 83 | + | atomNetWMStateMaximizedHorz: "_NET_WM_STATE_MAXIMIZED_HORZ", | |
| 84 | + | atomNetActiveWindow: "_NET_ACTIVE_WINDOW", | |
| 85 | + | atomNetClientList: "_NET_CLIENT_LIST", | |
| 86 | + | atomNetClientListStacking: "_NET_CLIENT_LIST_STACKING", | |
| 87 | + | atomNetWMMoveresize: "_NET_WM_MOVERESIZE", | |
| 88 | + | atomNetWMBypassCompositor: "_NET_WM_BYPASS_COMPOSITOR", | |
| 89 | + | atomWLSurfaceID: "WL_SURFACE_ID", | |
| 90 | + | atomWLSurfaceSerial: "WL_SURFACE_SERIAL", | |
| 91 | + | atomNetWMIcon: "_NET_WM_ICON", | |
| 92 | + | atomMotifWMHints: "_MOTIF_WM_HINTS", | |
| 93 | + | atomClipboard: "CLIPBOARD", | |
| 94 | + | atomPrimary: "PRIMARY", | |
| 95 | + | atomTargets: "TARGETS", | |
| 96 | + | } | |
A internal/adapters/wayland/xwm/configure.go +266 -0
| @@ -0,0 +1,266 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "math" | |
| 5 | + | ||
| 6 | + | "bnema.dev/xturbo" | |
| 7 | + | "bnema.dev/xturbo/xproto" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // maxClientSize bounds the size a floating window may ask for. | |
| 12 | + | const maxClientSize = 16384 | |
| 13 | + | ||
| 14 | + | // _NET_WM_STATE bits tracked in record.stateSent. | |
| 15 | + | const ( | |
| 16 | + | stateFullscreen uint8 = 1 << iota | |
| 17 | + | stateFocused | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // size is the size to send: the requested one, else the last known. | |
| 21 | + | func (r *record) size() (w, h uint32) { | |
| 22 | + | w, h = r.pend.w, r.pend.h | |
| 23 | + | if w == 0 { | |
| 24 | + | w = r.w | |
| 25 | + | } | |
| 26 | + | if h == 0 { | |
| 27 | + | h = r.h | |
| 28 | + | } | |
| 29 | + | return w, h | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // differs reports whether the pending configure is not what the X server has. | |
| 33 | + | func (r *record) differs() bool { | |
| 34 | + | w, h := r.size() | |
| 35 | + | p, s := &r.pend, &r.sent | |
| 36 | + | return p.x != s.x || p.y != s.y || w != s.w || h != s.h || | |
| 37 | + | p.fullscreen != s.fullscreen || p.activated != s.activated | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // markDirty queues s for the next flush when its pending configure differs. | |
| 41 | + | func (x *XWM) markDirty(s Slot, r *record) { | |
| 42 | + | if r.pend.dirty || !r.differs() { | |
| 43 | + | return | |
| 44 | + | } | |
| 45 | + | if len(x.dirty) == cap(x.dirty) { | |
| 46 | + | // Stale entries of freed and reused slots filled the queue: send | |
| 47 | + | // what is queued rather than grow it. r stays valid, nothing here | |
| 48 | + | // grows the table. | |
| 49 | + | x.flushConfigures() | |
| 50 | + | } | |
| 51 | + | r.pend.dirty = true | |
| 52 | + | x.dirty = append(x.dirty, s) | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // managedRec returns the record of a live managed window. | |
| 56 | + | func (x *XWM) managedRec(s Slot) *record { | |
| 57 | + | if x.closed { | |
| 58 | + | return nil | |
| 59 | + | } | |
| 60 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged { | |
| 61 | + | return r | |
| 62 | + | } | |
| 63 | + | return nil | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Configure records the size and state core wants for s; sent at flush. A | |
| 67 | + | // zero size keeps the current one. | |
| 68 | + | func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { | |
| 69 | + | r := x.managedRec(s) | |
| 70 | + | if r == nil { | |
| 71 | + | return | |
| 72 | + | } | |
| 73 | + | if c.Width > 0 { | |
| 74 | + | r.pend.w = uint32(min(c.Width, maxClientSize)) | |
| 75 | + | } | |
| 76 | + | if c.Height > 0 { | |
| 77 | + | r.pend.h = uint32(min(c.Height, maxClientSize)) | |
| 78 | + | } | |
| 79 | + | r.pend.fullscreen, r.pend.activated = c.Fullscreen, c.Activated | |
| 80 | + | x.markDirty(s, r) | |
| 81 | + | x.wake() | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Place records the layout position of the client rectangle of s. | |
| 85 | + | func (x *XWM) Place(s Slot, px, py int) { | |
| 86 | + | r := x.managedRec(s) | |
| 87 | + | if r == nil { | |
| 88 | + | return | |
| 89 | + | } | |
| 90 | + | r.pend.x, r.pend.y = int32(px), int32(py) | |
| 91 | + | x.markDirty(s, r) | |
| 92 | + | x.wake() | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | // flushConfigures sends the pending configures, once per display iteration, | |
| 96 | + | // then what focus left to do. | |
| 97 | + | func (x *XWM) flushConfigures() { | |
| 98 | + | for _, s := range x.dirty { | |
| 99 | + | if r := x.tab.rec(s); r != nil && r.kind == kindManaged && r.pend.dirty { | |
| 100 | + | x.sendPending(s, r) | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | x.dirty = x.dirty[:0] | |
| 104 | + | x.flushFocus() | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // sendPending writes the geometry (with the synthetic ConfigureNotify ICCCM | |
| 108 | + | // 4.1.5 asks for, since Xwayland may not move the window) and the | |
| 109 | + | // _NET_WM_STATE bits that changed. | |
| 110 | + | func (x *XWM) sendPending(s Slot, r *record) { | |
| 111 | + | r.pend.dirty = false | |
| 112 | + | p := &r.pend | |
| 113 | + | w, h := r.size() | |
| 114 | + | if w > 0 && h > 0 && (p.x != r.sent.x || p.y != r.sent.y || w != r.sent.w || h != r.sent.h) { | |
| 115 | + | var v xproto.ConfigureWindowValues | |
| 116 | + | v.SetX(p.x).SetY(p.y).SetWidth(w).SetHeight(h).SetBorderWidth(0) | |
| 117 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0) | |
| 118 | + | r.x, r.y, r.w, r.h = p.x, p.y, w, h | |
| 119 | + | x.sendConfigureNotify(r) | |
| 120 | + | r.sent.x, r.sent.y, r.sent.w, r.sent.h = p.x, p.y, w, h | |
| 121 | + | } | |
| 122 | + | var bits uint8 | |
| 123 | + | if p.fullscreen { | |
| 124 | + | bits |= stateFullscreen | |
| 125 | + | } | |
| 126 | + | if p.activated { | |
| 127 | + | bits |= stateFocused | |
| 128 | + | } | |
| 129 | + | r.sent.fullscreen, r.sent.activated = p.fullscreen, p.activated | |
| 130 | + | r.fullscreen = p.fullscreen // ours now: the property is not read back | |
| 131 | + | if bits != r.stateSent { | |
| 132 | + | r.stateSent = bits | |
| 133 | + | x.writeState(r) | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // writeState rewrites _NET_WM_STATE of r from its bits. | |
| 138 | + | func (x *XWM) writeState(r *record) { | |
| 139 | + | var list [3]uint32 | |
| 140 | + | n := 0 | |
| 141 | + | if r.modal { | |
| 142 | + | list[n] = x.atoms[atomNetWMStateModal] | |
| 143 | + | n++ | |
| 144 | + | } | |
| 145 | + | if r.stateSent&stateFullscreen != 0 { | |
| 146 | + | list[n] = x.atoms[atomNetWMStateFullscreen] | |
| 147 | + | n++ | |
| 148 | + | } | |
| 149 | + | if r.stateSent&stateFocused != 0 { | |
| 150 | + | list[n] = x.atoms[atomNetWMStateFocused] | |
| 151 | + | n++ | |
| 152 | + | } | |
| 153 | + | x.setProp32(r.xid, atomNetWMState, xproto.AtomAtom, list[:n]) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // sendConfigureNotify tells the client its geometry as the XWM sees it. | |
| 157 | + | func (x *XWM) sendConfigureNotify(r *record) { | |
| 158 | + | var raw [32]byte | |
| 159 | + | d := xproto.ConfigureNotifyEventData{ | |
| 160 | + | Event: xproto.Window(r.xid), Window: xproto.Window(r.xid), | |
| 161 | + | X: int16(min(max(r.x, math.MinInt16), math.MaxInt16)), Y: int16(min(max(r.y, math.MinInt16), math.MaxInt16)), | |
| 162 | + | Width: uint16(min(r.w, math.MaxUint16)), Height: uint16(min(r.h, math.MaxUint16)), | |
| 163 | + | } | |
| 164 | + | d.Put(&raw) | |
| 165 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(r.xid), xproto.EventMaskStructureNotify, &raw) | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // onConfigureRequest answers a client resizing itself. Tiled windows are told | |
| 169 | + | // their current geometry; floating ones get the size they ask for; unmanaged | |
| 170 | + | // and unknown windows are configured as requested. | |
| 171 | + | func (x *XWM) onConfigureRequest(ev *xturbo.Event) { | |
| 172 | + | e := xproto.ConfigureRequestOf(ev) | |
| 173 | + | xid := uint32(e.Window()) | |
| 174 | + | mask := e.ValueMask() | |
| 175 | + | s := x.tab.lookup(xid) | |
| 176 | + | r := x.managedRec(s) | |
| 177 | + | if r == nil { | |
| 178 | + | var v xproto.ConfigureWindowValues | |
| 179 | + | if mask&xproto.ConfigWindowX != 0 { | |
| 180 | + | v.SetX(int32(e.X())) | |
| 181 | + | } | |
| 182 | + | if mask&xproto.ConfigWindowY != 0 { | |
| 183 | + | v.SetY(int32(e.Y())) | |
| 184 | + | } | |
| 185 | + | if mask&xproto.ConfigWindowWidth != 0 { | |
| 186 | + | v.SetWidth(uint32(e.Width())) | |
| 187 | + | } | |
| 188 | + | if mask&xproto.ConfigWindowHeight != 0 { | |
| 189 | + | v.SetHeight(uint32(e.Height())) | |
| 190 | + | } | |
| 191 | + | if mask&xproto.ConfigWindowBorderWidth != 0 { | |
| 192 | + | v.SetBorderWidth(uint32(e.BorderWidth())) | |
| 193 | + | } | |
| 194 | + | if mask&xproto.ConfigWindowSibling != 0 { | |
| 195 | + | v.SetSibling(e.Sibling()) | |
| 196 | + | } | |
| 197 | + | if mask&xproto.ConfigWindowStackMode != 0 { | |
| 198 | + | v.SetStackMode(e.StackMode()) | |
| 199 | + | } | |
| 200 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v) | |
| 201 | + | return | |
| 202 | + | } | |
| 203 | + | if !r.ready { | |
| 204 | + | x.configureBeforeReady(s, r, e) | |
| 205 | + | return | |
| 206 | + | } | |
| 207 | + | if r.floating && mask&(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 { | |
| 208 | + | w, h := r.w, r.h | |
| 209 | + | if mask&xproto.ConfigWindowWidth != 0 { | |
| 210 | + | w = uint32(min(max(int(e.Width()), 1), maxClientSize)) | |
| 211 | + | } | |
| 212 | + | if mask&xproto.ConfigWindowHeight != 0 { | |
| 213 | + | h = uint32(min(max(int(e.Height()), 1), maxClientSize)) | |
| 214 | + | } | |
| 215 | + | if w > 0 && h > 0 { | |
| 216 | + | var v xproto.ConfigureWindowValues | |
| 217 | + | v.SetWidth(w).SetHeight(h) | |
| 218 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v), kindConfigure, x.tab.tag(s), 0) | |
| 219 | + | r.w, r.h = w, h | |
| 220 | + | // The size is the client's own: core learns it from the commit | |
| 221 | + | // and Configure only resends it when core wants another. | |
| 222 | + | r.pend.w, r.pend.h = w, h | |
| 223 | + | r.sent.w, r.sent.h = w, h | |
| 224 | + | } | |
| 225 | + | } | |
| 226 | + | x.sendConfigureNotify(r) | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | // configureBeforeReady honours the geometry a window asks for before it is | |
| 230 | + | // ready (XMoveResize before XMapWindow), floating or not: core only sees the | |
| 231 | + | // window at Ready and the client must find the geometry it set. | |
| 232 | + | func (x *XWM) configureBeforeReady(s Slot, r *record, e xproto.ConfigureRequestEvent) { | |
| 233 | + | mask := e.ValueMask() | |
| 234 | + | var v xproto.ConfigureWindowValues | |
| 235 | + | if mask&xproto.ConfigWindowX != 0 { | |
| 236 | + | r.x = int32(e.X()) | |
| 237 | + | v.SetX(r.x) | |
| 238 | + | } | |
| 239 | + | if mask&xproto.ConfigWindowY != 0 { | |
| 240 | + | r.y = int32(e.Y()) | |
| 241 | + | v.SetY(r.y) | |
| 242 | + | } | |
| 243 | + | if mask&xproto.ConfigWindowWidth != 0 { | |
| 244 | + | r.w, r.sized = uint32(min(max(int(e.Width()), 1), maxClientSize)), true | |
| 245 | + | v.SetWidth(r.w) | |
| 246 | + | } | |
| 247 | + | if mask&xproto.ConfigWindowHeight != 0 { | |
| 248 | + | r.h, r.sized = uint32(min(max(int(e.Height()), 1), maxClientSize)), true | |
| 249 | + | v.SetHeight(r.h) | |
| 250 | + | } | |
| 251 | + | if mask&(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 { | |
| 252 | + | x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0) | |
| 253 | + | } | |
| 254 | + | x.sendConfigureNotify(r) | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | // onConfigureNotify keeps the known geometry of every top-level window. | |
| 258 | + | func (x *XWM) onConfigureNotify(ev *xturbo.Event) { | |
| 259 | + | e := xproto.ConfigureNotifyOf(ev) | |
| 260 | + | if e.Event() != xproto.Window(x.root) { | |
| 261 | + | return | |
| 262 | + | } | |
| 263 | + | if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree { | |
| 264 | + | r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height()) | |
| 265 | + | } | |
| 266 | + | } | |
A internal/adapters/wayland/xwm/configure_test.go +565 -0
| @@ -0,0 +1,565 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "bnema.dev/xturbo/xproto" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | "github.com/stretchr/testify/mock" | |
| 10 | + | "github.com/stretchr/testify/require" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // newWin makes a ready managed window known to the XWM and the mocked host. | |
| 14 | + | func newWin(t *testing.T, x *XWM, p *xpeer, h *mockhost, xid uint32) Slot { | |
| 15 | + | t.Helper() | |
| 16 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(xid)).Once() | |
| 17 | + | p.createNotify(xid, false) | |
| 18 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 }) | |
| 19 | + | s := x.tab.lookup(xid) | |
| 20 | + | r := x.tab.rec(s) | |
| 21 | + | r.ready, r.w, r.h = true, 300, 200 | |
| 22 | + | return s | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // settle flushes the XWM and waits until the peer has seen everything sent so | |
| 26 | + | // far; it returns the number of requests received. | |
| 27 | + | func settle(t *testing.T, x *XWM, p *xpeer) int { | |
| 28 | + | t.Helper() | |
| 29 | + | x.flush() | |
| 30 | + | n := len(p.requests()) | |
| 31 | + | xproto.GetInputFocus(x.conn.Buf()) | |
| 32 | + | require.NoError(t, x.conn.Flush()) | |
| 33 | + | _, n = p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |
| 34 | + | return n | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // since returns the requests of the given major opcode received after skip. | |
| 38 | + | func since(p *xpeer, skip int, major uint8) []xpeerReq { | |
| 39 | + | var out []xpeerReq | |
| 40 | + | for _, r := range p.requests()[skip:] { | |
| 41 | + | if r.major == major { | |
| 42 | + | out = append(out, r) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | return out | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // sentEvents returns the 32-byte events of the SendEvent requests to dest. | |
| 49 | + | func sentEvents(p *xpeer, skip int, dest uint32) [][]byte { | |
| 50 | + | var out [][]byte | |
| 51 | + | for _, r := range since(p, skip, opSendEvent) { | |
| 52 | + | if r.u32(4) == dest { | |
| 53 | + | out = append(out, r.raw[12:44]) | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | return out | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func propsTo(p *xpeer, skip int, win uint32, atom string) []xpeerReq { | |
| 60 | + | var out []xpeerReq | |
| 61 | + | a := p.atom(atom) | |
| 62 | + | for _, r := range since(p, skip, opChangeProperty) { | |
| 63 | + | if r.u32(4) == win && r.u32(8) == a { | |
| 64 | + | out = append(out, r) | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | return out | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (x *XWM) feed(t *testing.T, p *xpeer, send func()) { | |
| 71 | + | t.Helper() | |
| 72 | + | ev := x.stats.Events | |
| 73 | + | send() | |
| 74 | + | readUntil(t, x, func() bool { return x.stats.Events > ev }) | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | func TestConfigureCoalescesIntoOneRequest(t *testing.T) { | |
| 78 | + | x, p, h, _, runs := attached(t) | |
| 79 | + | const win = 0x500001 | |
| 80 | + | s := newWin(t, x, p, h, win) | |
| 81 | + | n := settle(t, x, p) | |
| 82 | + | ||
| 83 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 84 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |
| 85 | + | x.Place(s, 10, 20) | |
| 86 | + | require.Len(t, *runs, 1, "one flush posted") | |
| 87 | + | n2 := settle(t, x, p) | |
| 88 | + | ||
| 89 | + | cw := since(p, n, opConfigureWindow) | |
| 90 | + | require.Len(t, cw, 1) | |
| 91 | + | require.Equal(t, uint32(win), cw[0].u32(4)) | |
| 92 | + | require.EqualValues(t, 0x1f, binary.LittleEndian.Uint16(cw[0].raw[8:]), "x y width height border") | |
| 93 | + | require.Equal(t, []uint32{10, 20, 800, 600, 0}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24), cw[0].u32(28)}) | |
| 94 | + | ||
| 95 | + | evs := sentEvents(p, n, win) | |
| 96 | + | require.Len(t, evs, 1, "one synthetic ConfigureNotify") | |
| 97 | + | require.EqualValues(t, 22, evs[0][0]) | |
| 98 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][4:])) | |
| 99 | + | require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][8:])) | |
| 100 | + | require.EqualValues(t, 10, binary.LittleEndian.Uint16(evs[0][16:])) | |
| 101 | + | require.EqualValues(t, 20, binary.LittleEndian.Uint16(evs[0][18:])) | |
| 102 | + | require.EqualValues(t, 800, binary.LittleEndian.Uint16(evs[0][20:])) | |
| 103 | + | require.EqualValues(t, 600, binary.LittleEndian.Uint16(evs[0][22:])) | |
| 104 | + | require.Equal(t, xproto.EventMaskStructureNotify, xproto.EventMask(since(p, n, opSendEvent)[0].u32(8))) | |
| 105 | + | ||
| 106 | + | // Unchanged: nothing is sent. | |
| 107 | + | x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true}) | |
| 108 | + | x.Place(s, 10, 20) | |
| 109 | + | settle(t, x, p) | |
| 110 | + | require.Len(t, since(p, n2, opConfigureWindow), 0) | |
| 111 | + | require.Len(t, sentEvents(p, n2, win), 0) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | func TestConfigureWritesNetWMState(t *testing.T) { | |
| 115 | + | x, p, h, _, _ := attached(t) | |
| 116 | + | const win = 0x500001 | |
| 117 | + | s := newWin(t, x, p, h, win) | |
| 118 | + | x.tab.rec(s).modal = true | |
| 119 | + | n := settle(t, x, p) | |
| 120 | + | ||
| 121 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100}) | |
| 122 | + | settle(t, x, p) | |
| 123 | + | require.Empty(t, propsTo(p, n, win, "_NET_WM_STATE"), "no state bit changed") | |
| 124 | + | ||
| 125 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Fullscreen: true, Activated: true}) | |
| 126 | + | settle(t, x, p) | |
| 127 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |
| 128 | + | require.True(t, ok) | |
| 129 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FULLSCREEN"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 130 | + | ||
| 131 | + | x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Activated: true}) | |
| 132 | + | settle(t, x, p) | |
| 133 | + | st, _ = p.prop(win, "_NET_WM_STATE") | |
| 134 | + | require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | func TestConfigureBadWindowIsAttributedAndIgnored(t *testing.T) { | |
| 138 | + | x, p, h, _, _ := attached(t) | |
| 139 | + | const win = 0x500001 | |
| 140 | + | s := newWin(t, x, p, h, win) | |
| 141 | + | n := settle(t, x, p) | |
| 142 | + | x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 143 | + | x.flush() | |
| 144 | + | cw := []xpeerReq{} | |
| 145 | + | r, _ := p.waitReq(n, func(r xpeerReq) bool { return r.major == opConfigureWindow }) | |
| 146 | + | cw = append(cw, r) | |
| 147 | + | ||
| 148 | + | p.mu.Lock() | |
| 149 | + | e := make([]byte, 32) | |
| 150 | + | e[1] = xproto.BadWindow | |
| 151 | + | binary.LittleEndian.PutUint16(e[2:], cw[0].seq) | |
| 152 | + | binary.LittleEndian.PutUint32(e[4:], win) | |
| 153 | + | e[10] = opConfigureWindow | |
| 154 | + | p.write(e) | |
| 155 | + | p.mu.Unlock() | |
| 156 | + | readUntil(t, x, func() bool { return x.stats.Errors == 1 }) | |
| 157 | + | require.Zero(t, x.stats.Unattributed) | |
| 158 | + | require.False(t, x.closed) | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | func sendConfigureRequest(p *xpeer, win uint32, mask uint16, x, y int16, w, h uint16) { | |
| 162 | + | p.event(23, func(b *[32]byte) { | |
| 163 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 164 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 165 | + | binary.LittleEndian.PutUint16(b[16:], uint16(x)) | |
| 166 | + | binary.LittleEndian.PutUint16(b[18:], uint16(y)) | |
| 167 | + | binary.LittleEndian.PutUint16(b[20:], w) | |
| 168 | + | binary.LittleEndian.PutUint16(b[22:], h) | |
| 169 | + | binary.LittleEndian.PutUint16(b[26:], mask) | |
| 170 | + | }) | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func TestConfigureRequestTiledOnlyGetsNotify(t *testing.T) { | |
| 174 | + | x, p, h, _, _ := attached(t) | |
| 175 | + | const win = 0x500001 | |
| 176 | + | newWin(t, x, p, h, win) | |
| 177 | + | n := settle(t, x, p) | |
| 178 | + | ||
| 179 | + | x.feed(t, p, func() { sendConfigureRequest(p, win, 0xf, 5, 6, 1000, 900) }) | |
| 180 | + | settle(t, x, p) | |
| 181 | + | require.Empty(t, since(p, n, opConfigureWindow), "a tiled window cannot resize itself") | |
| 182 | + | evs := sentEvents(p, n, win) | |
| 183 | + | require.Len(t, evs, 1) | |
| 184 | + | require.EqualValues(t, 22, evs[0][0]) | |
| 185 | + | require.EqualValues(t, 300, binary.LittleEndian.Uint16(evs[0][20:]), "current geometry") | |
| 186 | + | require.EqualValues(t, 200, binary.LittleEndian.Uint16(evs[0][22:])) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | func TestConfigureRequestFloatingHonoursSize(t *testing.T) { | |
| 190 | + | x, p, h, _, _ := attached(t) | |
| 191 | + | const win = 0x500001 | |
| 192 | + | s := newWin(t, x, p, h, win) | |
| 193 | + | x.tab.rec(s).floating = true | |
| 194 | + | n := settle(t, x, p) | |
| 195 | + | ||
| 196 | + | x.feed(t, p, func() { | |
| 197 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight), 0, 0, 70000&0xffff, 20000) | |
| 198 | + | }) | |
| 199 | + | settle(t, x, p) | |
| 200 | + | cw := since(p, n, opConfigureWindow) | |
| 201 | + | require.Len(t, cw, 1) | |
| 202 | + | require.EqualValues(t, xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |
| 203 | + | require.EqualValues(t, 4464, cw[0].u32(12)) | |
| 204 | + | require.EqualValues(t, maxClientSize, cw[0].u32(16), "clamped") | |
| 205 | + | require.Len(t, sentEvents(p, n, win), 1) | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | func TestConfigureRequestUnmanagedHonouredVerbatim(t *testing.T) { | |
| 209 | + | x, p, _, _, _ := attached(t) | |
| 210 | + | const win = 0x500009 | |
| 211 | + | p.createNotify(win, true) | |
| 212 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 }) | |
| 213 | + | n := settle(t, x, p) | |
| 214 | + | ||
| 215 | + | x.feed(t, p, func() { | |
| 216 | + | sendConfigureRequest(p, win, uint16(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth), 30, 40, 50, 60) | |
| 217 | + | }) | |
| 218 | + | settle(t, x, p) | |
| 219 | + | cw := since(p, n, opConfigureWindow) | |
| 220 | + | require.Len(t, cw, 1) | |
| 221 | + | require.EqualValues(t, 7, binary.LittleEndian.Uint16(cw[0].raw[8:])) | |
| 222 | + | require.Equal(t, []uint32{30, 40, 50}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20)}) | |
| 223 | + | require.Empty(t, sentEvents(p, n, win)) | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | func TestConfigureStaleAndDuplicateDirtySlots(t *testing.T) { | |
| 227 | + | x, p, h, _, _ := attached(t) | |
| 228 | + | const a, b = 0x500001, 0x500002 | |
| 229 | + | s := newWin(t, x, p, h, a) | |
| 230 | + | for i := 1; i <= 50; i++ { | |
| 231 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i, Height: 100}) | |
| 232 | + | } | |
| 233 | + | require.Len(t, x.dirty, 1, "queued once per false→true transition") | |
| 234 | + | ||
| 235 | + | // The window dies with its entry queued; the slot is reused. | |
| 236 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 237 | + | p.destroyNotify(a) | |
| 238 | + | readUntil(t, x, func() bool { return x.tab.lookup(a) == 0 }) | |
| 239 | + | s2 := newWin(t, x, p, h, b) | |
| 240 | + | require.Equal(t, s, s2, "LIFO slot reuse") | |
| 241 | + | n := len(p.requests()) | |
| 242 | + | x.Configure(s2, ports.ConfigureWindow{Width: 640, Height: 480}) | |
| 243 | + | require.Len(t, x.dirty, 1) | |
| 244 | + | x.dirty = append(x.dirty, s, s) // stale and duplicate entries must be skipped | |
| 245 | + | settle(t, x, p) | |
| 246 | + | cw := since(p, n, opConfigureWindow) | |
| 247 | + | require.Len(t, cw, 1, "sent once despite duplicates") | |
| 248 | + | require.Equal(t, uint32(b), cw[0].u32(4)) | |
| 249 | + | require.Empty(t, x.dirty) | |
| 250 | + | require.LessOrEqual(t, cap(x.dirty), len(x.tab.recs)) | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | func TestDirtyQueueNeverGrows(t *testing.T) { | |
| 254 | + | x, p, h, _, _ := attached(t) | |
| 255 | + | c0 := cap(x.dirty) | |
| 256 | + | const base = 0x600000 | |
| 257 | + | for i := 0; i < 3*c0; i++ { // each round leaves a stale entry behind | |
| 258 | + | xid := uint32(base + i) | |
| 259 | + | h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(1)).Once() | |
| 260 | + | p.createNotify(xid, false) | |
| 261 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 }) | |
| 262 | + | s := x.tab.lookup(xid) | |
| 263 | + | x.tab.rec(s).ready = true | |
| 264 | + | x.Configure(s, ports.ConfigureWindow{Width: 10, Height: 10}) | |
| 265 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 266 | + | p.destroyNotify(xid) | |
| 267 | + | readUntil(t, x, func() bool { return x.tab.lookup(xid) == 0 }) | |
| 268 | + | } | |
| 269 | + | require.Equal(t, c0, cap(x.dirty)) | |
| 270 | + | require.LessOrEqual(t, len(x.dirty), c0) | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | func TestPublicCommandsIgnoredWhenClosed(t *testing.T) { | |
| 274 | + | x, p, h, src, _ := attached(t) | |
| 275 | + | const win = 0x500001 | |
| 276 | + | s := newWin(t, x, p, h, win) | |
| 277 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 278 | + | src.EXPECT().Remove().Once() | |
| 279 | + | x.Close() | |
| 280 | + | require.NotPanics(t, func() { | |
| 281 | + | x.Configure(s, ports.ConfigureWindow{Width: 1, Height: 1}) | |
| 282 | + | x.Place(s, 1, 2) | |
| 283 | + | x.Focus(s) | |
| 284 | + | x.CloseWindow(s) | |
| 285 | + | }) | |
| 286 | + | require.Empty(t, x.dirty) | |
| 287 | + | require.Zero(t, x.active) | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | // Focus. | |
| 291 | + | ||
| 292 | + | type focusModel struct { | |
| 293 | + | name string | |
| 294 | + | input, take bool | |
| 295 | + | setFocus bool // SetInputFocus on the window | |
| 296 | + | takeFocus bool // WM_TAKE_FOCUS sent | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | func TestFocusInputModels(t *testing.T) { | |
| 300 | + | for _, m := range []focusModel{ | |
| 301 | + | {"passive", true, false, true, false}, | |
| 302 | + | {"locally active", true, true, true, true}, | |
| 303 | + | {"globally active", false, true, false, true}, | |
| 304 | + | {"no input", false, false, false, false}, | |
| 305 | + | } { | |
| 306 | + | t.Run(m.name, func(t *testing.T) { | |
| 307 | + | x, p, h, _, _ := attached(t) | |
| 308 | + | const win = 0x500001 | |
| 309 | + | s := newWin(t, x, p, h, win) | |
| 310 | + | r := x.tab.rec(s) | |
| 311 | + | r.input, r.takeFocus = m.input, m.take | |
| 312 | + | n := settle(t, x, p) | |
| 313 | + | ||
| 314 | + | x.Focus(s) | |
| 315 | + | settle(t, x, p) | |
| 316 | + | var focuses []xpeerReq | |
| 317 | + | for _, r := range since(p, n, opSetInputFocus) { | |
| 318 | + | if r.u32(4) == win { | |
| 319 | + | focuses = append(focuses, r) | |
| 320 | + | } | |
| 321 | + | } | |
| 322 | + | if m.setFocus { | |
| 323 | + | require.Len(t, focuses, 1) | |
| 324 | + | require.EqualValues(t, xproto.InputFocusNone, focuses[0].raw[1], "never PointerRoot") | |
| 325 | + | require.Zero(t, focuses[0].u32(8), "CurrentTime") | |
| 326 | + | } else { | |
| 327 | + | require.Empty(t, focuses) | |
| 328 | + | } | |
| 329 | + | var takes int | |
| 330 | + | for _, ev := range sentEvents(p, n, win) { | |
| 331 | + | if ev[0] == 33 && binary.LittleEndian.Uint32(ev[8:]) == p.atom("WM_PROTOCOLS") { | |
| 332 | + | require.EqualValues(t, 32, ev[1]) | |
| 333 | + | require.Equal(t, p.atom("WM_TAKE_FOCUS"), binary.LittleEndian.Uint32(ev[12:])) | |
| 334 | + | require.Zero(t, binary.LittleEndian.Uint32(ev[16:]), "CurrentTime") | |
| 335 | + | takes++ | |
| 336 | + | } | |
| 337 | + | } | |
| 338 | + | require.Equal(t, map[bool]int{true: 1, false: 0}[m.takeFocus], takes) | |
| 339 | + | ||
| 340 | + | act, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 341 | + | require.True(t, ok) | |
| 342 | + | require.Equal(t, []uint32{win}, atomsOf(act), "named even for No-input windows") | |
| 343 | + | st, ok := p.prop(win, "_NET_WM_STATE") | |
| 344 | + | require.True(t, ok) | |
| 345 | + | require.Contains(t, atomsOf(st), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 346 | + | }) | |
| 347 | + | } | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | func TestFocusActiveWindowWrittenOnlyOnChange(t *testing.T) { | |
| 351 | + | x, p, h, _, _ := attached(t) | |
| 352 | + | const a, b = 0x500001, 0x500002 | |
| 353 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |
| 354 | + | n := settle(t, x, p) | |
| 355 | + | ||
| 356 | + | x.Focus(sa) | |
| 357 | + | x.Focus(sa) | |
| 358 | + | settle(t, x, p) | |
| 359 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_ACTIVE_WINDOW"), 1) | |
| 360 | + | ||
| 361 | + | x.Focus(sb) | |
| 362 | + | settle(t, x, p) | |
| 363 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 364 | + | require.Equal(t, []uint32{b}, atomsOf(act)) | |
| 365 | + | sta, _ := p.prop(a, "_NET_WM_STATE") | |
| 366 | + | require.NotContains(t, atomsOf(sta), p.atom("_NET_WM_STATE_FOCUSED"), "old window loses FOCUSED") | |
| 367 | + | stb, _ := p.prop(b, "_NET_WM_STATE") | |
| 368 | + | require.Contains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 369 | + | ||
| 370 | + | n = settle(t, x, p) | |
| 371 | + | x.Focus(0) | |
| 372 | + | settle(t, x, p) | |
| 373 | + | none := since(p, n, opSetInputFocus) | |
| 374 | + | require.Len(t, none, 1) | |
| 375 | + | require.Zero(t, none[0].u32(4), "focus None") | |
| 376 | + | require.EqualValues(t, xproto.InputFocusNone, none[0].raw[1]) | |
| 377 | + | act, _ = p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 378 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |
| 379 | + | stb, _ = p.prop(b, "_NET_WM_STATE") | |
| 380 | + | require.NotContains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED")) | |
| 381 | + | require.Zero(t, x.active) | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | func TestFocusStackingLists(t *testing.T) { | |
| 385 | + | x, p, h, _, _ := attached(t) | |
| 386 | + | const a, b, c = 0x500001, 0x500002, 0x500003 | |
| 387 | + | sa := newWin(t, x, p, h, a) | |
| 388 | + | newWin(t, x, p, h, b) | |
| 389 | + | newWin(t, x, p, h, c) | |
| 390 | + | for _, w := range []uint32{a, b, c} { | |
| 391 | + | x.addClient(w) | |
| 392 | + | } | |
| 393 | + | settle(t, x, p) | |
| 394 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 395 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list)) | |
| 396 | + | n := settle(t, x, p) | |
| 397 | + | ||
| 398 | + | x.Focus(sa) | |
| 399 | + | settle(t, x, p) | |
| 400 | + | stack, _ := p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |
| 401 | + | require.Equal(t, []uint32{b, c, a}, atomsOf(stack), "bottom to top") | |
| 402 | + | list, _ = p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 403 | + | require.Equal(t, []uint32{a, b, c}, atomsOf(list), "map order") | |
| 404 | + | require.Empty(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), "clients list untouched by focus") | |
| 405 | + | var raised bool | |
| 406 | + | for _, r := range since(p, n, opConfigureWindow) { | |
| 407 | + | if r.u32(4) == a && binary.LittleEndian.Uint16(r.raw[8:]) == uint16(xproto.ConfigWindowStackMode) { | |
| 408 | + | require.EqualValues(t, xproto.StackModeAbove, r.u32(12)) | |
| 409 | + | raised = true | |
| 410 | + | } | |
| 411 | + | } | |
| 412 | + | require.True(t, raised) | |
| 413 | + | ||
| 414 | + | // A burst costs one write per list. | |
| 415 | + | n = settle(t, x, p) | |
| 416 | + | x.removeClient(b) | |
| 417 | + | x.removeClient(c) | |
| 418 | + | settle(t, x, p) | |
| 419 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), 1) | |
| 420 | + | require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST_STACKING"), 1) | |
| 421 | + | stack, _ = p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING") | |
| 422 | + | require.Equal(t, []uint32{a}, atomsOf(stack)) | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | func TestDestroyedActiveWindowClearsActive(t *testing.T) { | |
| 426 | + | x, p, h, _, _ := attached(t) | |
| 427 | + | const win = 0x500001 | |
| 428 | + | s := newWin(t, x, p, h, win) | |
| 429 | + | x.addClient(win) | |
| 430 | + | x.Focus(s) | |
| 431 | + | settle(t, x, p) | |
| 432 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 433 | + | p.destroyNotify(win) | |
| 434 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 }) | |
| 435 | + | settle(t, x, p) | |
| 436 | + | require.Zero(t, x.active) | |
| 437 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 438 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |
| 439 | + | list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 440 | + | require.Empty(t, list.data) | |
| 441 | + | } | |
| 442 | + | ||
| 443 | + | func TestFocusInDenied(t *testing.T) { | |
| 444 | + | x, p, h, _, _ := attached(t) | |
| 445 | + | const a, b = 0x500001, 0x500002 | |
| 446 | + | sa := newWin(t, x, p, h, a) | |
| 447 | + | newWin(t, x, p, h, b) | |
| 448 | + | x.Focus(sa) | |
| 449 | + | n := settle(t, x, p) | |
| 450 | + | ||
| 451 | + | focusIn := func(win uint32, detail byte) { | |
| 452 | + | p.event(9, func(e *[32]byte) { | |
| 453 | + | e[1] = detail | |
| 454 | + | binary.LittleEndian.PutUint32(e[4:], win) | |
| 455 | + | }) | |
| 456 | + | } | |
| 457 | + | focusCount := func(skip int) int { | |
| 458 | + | c := 0 | |
| 459 | + | for _, r := range since(p, skip, opSetInputFocus) { | |
| 460 | + | if r.u32(4) == a { | |
| 461 | + | c++ | |
| 462 | + | } | |
| 463 | + | } | |
| 464 | + | return c | |
| 465 | + | } | |
| 466 | + | x.feed(t, p, func() { focusIn(a, 0) }) // the active window itself: fine | |
| 467 | + | x.feed(t, p, func() { focusIn(b, byte(xproto.NotifyDetailPointer)) }) | |
| 468 | + | settle(t, x, p) | |
| 469 | + | require.Zero(t, focusCount(n)) | |
| 470 | + | ||
| 471 | + | x.feed(t, p, func() { focusIn(b, 0); focusIn(b, 0) }) | |
| 472 | + | readUntil(t, x, func() bool { return x.stats.Events >= 5 }) | |
| 473 | + | settle(t, x, p) | |
| 474 | + | require.Equal(t, 1, focusCount(n), "denied once per iteration") | |
| 475 | + | n = settle(t, x, p) | |
| 476 | + | settle(t, x, p) | |
| 477 | + | require.Zero(t, focusCount(n)) | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | func TestCloseWindow(t *testing.T) { | |
| 481 | + | x, p, h, _, _ := attached(t) | |
| 482 | + | const a, b = 0x500001, 0x500002 | |
| 483 | + | sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b) | |
| 484 | + | x.tab.rec(sa).deleteWindow = true | |
| 485 | + | n := settle(t, x, p) | |
| 486 | + | ||
| 487 | + | x.CloseWindow(sa) | |
| 488 | + | x.CloseWindow(sb) | |
| 489 | + | settle(t, x, p) | |
| 490 | + | evs := sentEvents(p, n, a) | |
| 491 | + | require.Len(t, evs, 1) | |
| 492 | + | require.EqualValues(t, 33, evs[0][0]) | |
| 493 | + | require.EqualValues(t, 32, evs[0][1]) | |
| 494 | + | require.Equal(t, p.atom("WM_PROTOCOLS"), binary.LittleEndian.Uint32(evs[0][8:])) | |
| 495 | + | require.Equal(t, p.atom("WM_DELETE_WINDOW"), binary.LittleEndian.Uint32(evs[0][12:])) | |
| 496 | + | require.Zero(t, binary.LittleEndian.Uint32(evs[0][16:])) | |
| 497 | + | require.Empty(t, sentEvents(p, n, b)) | |
| 498 | + | ||
| 499 | + | kills := since(p, n, opKillClient) | |
| 500 | + | require.Len(t, kills, 1) | |
| 501 | + | require.Equal(t, uint32(b), kills[0].u32(4)) | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | // Allocation guards. The peer sinks requests so the connection never blocks; | |
| 505 | + | // its goroutine is idle in the sink loop before measuring. | |
| 506 | + | ||
| 507 | + | func sinkPeer(t *testing.T, x *XWM, p *xpeer) { | |
| 508 | + | t.Helper() | |
| 509 | + | p.sink.Store(true) | |
| 510 | + | n := len(p.requests()) | |
| 511 | + | xproto.GetInputFocus(x.conn.Buf()) // served by the old loop iteration | |
| 512 | + | require.NoError(t, x.conn.Flush()) | |
| 513 | + | p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |
| 514 | + | // The peer records a request and answers it under mu: once the lock is | |
| 515 | + | // free again it is back in its sink loop, which does not allocate. | |
| 516 | + | p.mu.Lock() | |
| 517 | + | _ = p.seq | |
| 518 | + | p.mu.Unlock() | |
| 519 | + | x.src = nil // no source: write backpressure would not allocate either | |
| 520 | + | x.post = func(func()) {} | |
| 521 | + | x.warnedRing = true // the sink never answers: Expect slots stay used | |
| 522 | + | } | |
| 523 | + | ||
| 524 | + | func TestConfigureFlushAllocations(t *testing.T) { | |
| 525 | + | x, p, h, _, _ := attached(t) | |
| 526 | + | s := newWin(t, x, p, h, 0x500001) | |
| 527 | + | sinkPeer(t, x, p) | |
| 528 | + | i := 0 | |
| 529 | + | allocs := testing.AllocsPerRun(300, func() { | |
| 530 | + | i++ | |
| 531 | + | x.Configure(s, ports.ConfigureWindow{Width: 100 + i%2, Height: 100, Fullscreen: i%3 == 0, Activated: i%2 == 0}) | |
| 532 | + | x.Place(s, i%5, i%7) | |
| 533 | + | x.posted = false | |
| 534 | + | x.flush() | |
| 535 | + | }) | |
| 536 | + | require.Zero(t, allocs) | |
| 537 | + | require.False(t, x.closed) | |
| 538 | + | } | |
| 539 | + | ||
| 540 | + | func TestFocusAllocations(t *testing.T) { | |
| 541 | + | x, p, h, _, _ := attached(t) | |
| 542 | + | a, b := newWin(t, x, p, h, 0x500001), newWin(t, x, p, h, 0x500002) | |
| 543 | + | x.addClient(x.tab.rec(a).xid) | |
| 544 | + | x.addClient(x.tab.rec(b).xid) | |
| 545 | + | x.tab.rec(b).takeFocus = true | |
| 546 | + | x.tab.rec(a).input, x.tab.rec(b).input = true, true | |
| 547 | + | sinkPeer(t, x, p) | |
| 548 | + | i := 0 | |
| 549 | + | allocs := testing.AllocsPerRun(150, func() { | |
| 550 | + | i++ | |
| 551 | + | switch i % 3 { | |
| 552 | + | case 0: | |
| 553 | + | x.Focus(a) | |
| 554 | + | case 1: | |
| 555 | + | x.Focus(b) | |
| 556 | + | default: | |
| 557 | + | x.Focus(0) | |
| 558 | + | } | |
| 559 | + | x.refocus = i%5 == 0 | |
| 560 | + | x.posted = false | |
| 561 | + | x.flush() | |
| 562 | + | }) | |
| 563 | + | require.Zero(t, allocs) | |
| 564 | + | require.False(t, x.closed) | |
| 565 | + | } | |
A internal/adapters/wayland/xwm/events.go +252 -0
| @@ -0,0 +1,252 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bnema.dev/xturbo" | |
| 5 | + | "bnema.dev/xturbo/composite" | |
| 6 | + | "bnema.dev/xturbo/res" | |
| 7 | + | "bnema.dev/xturbo/xfixes" | |
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Reply kinds registered with Expect. The first argument is the window tag | |
| 12 | + | // (slot | generation<<16, see table.tag), the second a property index. | |
| 13 | + | const ( | |
| 14 | + | kindAttr uint16 = 16 + iota | |
| 15 | + | kindProp | |
| 16 | + | kindGeom | |
| 17 | + | kindClientIds | |
| 18 | + | kindConfigure | |
| 19 | + | kindFocus | |
| 20 | + | kindRootMask | |
| 21 | + | kindVersion | |
| 22 | + | kindTree | |
| 23 | + | ) | |
| 24 | + | ||
| 25 | + | // onEvent dispatches an X event. Before Attach there is no host: only the | |
| 26 | + | // table is maintained, and Attach replays the result. | |
| 27 | + | func (x *XWM) onEvent(ev *xturbo.Event) { | |
| 28 | + | x.stats.Events++ | |
| 29 | + | if x.closed { | |
| 30 | + | return | |
| 31 | + | } | |
| 32 | + | switch ev.Kind { | |
| 33 | + | case xproto.KindCreateNotify: | |
| 34 | + | x.onCreateNotify(ev) | |
| 35 | + | case xproto.KindDestroyNotify: | |
| 36 | + | x.onDestroyNotify(ev) | |
| 37 | + | case xproto.KindReparentNotify: | |
| 38 | + | x.onReparentNotify(ev) | |
| 39 | + | } | |
| 40 | + | if x.host == nil { | |
| 41 | + | x.onEventDetached(ev) | |
| 42 | + | return | |
| 43 | + | } | |
| 44 | + | switch ev.Kind { | |
| 45 | + | case xproto.KindMapRequest: | |
| 46 | + | x.onMapRequest(ev) | |
| 47 | + | case xproto.KindMapNotify: | |
| 48 | + | x.onMapNotify(ev) | |
| 49 | + | case xproto.KindUnmapNotify: | |
| 50 | + | x.onUnmapNotify(ev) | |
| 51 | + | case xproto.KindConfigureRequest: | |
| 52 | + | x.onConfigureRequest(ev) | |
| 53 | + | case xproto.KindConfigureNotify: | |
| 54 | + | x.onConfigureNotify(ev) | |
| 55 | + | case xproto.KindPropertyNotify: | |
| 56 | + | x.onPropertyNotify(ev) | |
| 57 | + | case xproto.KindClientMessage: | |
| 58 | + | x.onClientMessage(ev) | |
| 59 | + | case xproto.KindFocusIn: | |
| 60 | + | x.onFocusIn(ev) | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // onEventDetached keeps what Attach replays: windows asked to map, and those | |
| 65 | + | // withdrawn again, so Attach skips them; geometry requests are answered. | |
| 66 | + | func (x *XWM) onEventDetached(ev *xturbo.Event) { | |
| 67 | + | switch ev.Kind { | |
| 68 | + | case xproto.KindMapRequest: | |
| 69 | + | if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 { | |
| 70 | + | x.tab.rec(s).mapping = true | |
| 71 | + | } | |
| 72 | + | case xproto.KindUnmapNotify: | |
| 73 | + | if ev.SendEvent { | |
| 74 | + | return | |
| 75 | + | } | |
| 76 | + | if s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window())); s != 0 { | |
| 77 | + | r := x.tab.rec(s) | |
| 78 | + | r.mapped, r.mapping = false, false | |
| 79 | + | } | |
| 80 | + | case xproto.KindConfigureRequest: | |
| 81 | + | x.onConfigureRequest(ev) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | func (x *XWM) onReply(r *xturbo.Reply) { | |
| 86 | + | x.stats.Replies++ | |
| 87 | + | if x.closed { | |
| 88 | + | return | |
| 89 | + | } | |
| 90 | + | switch r.Kind { | |
| 91 | + | case kindVersion: | |
| 92 | + | x.onVersionReply(r) | |
| 93 | + | return | |
| 94 | + | case kindTree: | |
| 95 | + | x.onTreeReply(r) | |
| 96 | + | return | |
| 97 | + | } | |
| 98 | + | s, rec := x.tab.live(r.Args[0]) | |
| 99 | + | if rec == nil { | |
| 100 | + | x.stats.Stale++ | |
| 101 | + | return | |
| 102 | + | } | |
| 103 | + | switch r.Kind { | |
| 104 | + | case kindAttr: | |
| 105 | + | x.onAttrReply(rec, r) | |
| 106 | + | case kindProp: | |
| 107 | + | x.onPropReply(s, rec, r) | |
| 108 | + | case kindGeom: | |
| 109 | + | x.onGeomReply(s, rec, r) | |
| 110 | + | case kindClientIds: | |
| 111 | + | x.onClientIdsReply(s, rec, r) | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | // onError never tears the XWM down: an error names a request, not the | |
| 116 | + | // connection. Sticky Buf errors surface through Flush. | |
| 117 | + | func (x *XWM) onError(e *xturbo.XError) { | |
| 118 | + | x.stats.Errors++ | |
| 119 | + | if e.Kind == 0 { | |
| 120 | + | x.stats.Unattributed++ | |
| 121 | + | x.log.Debug().Uint8("code", e.Code).Uint8("major", e.Major).Uint16("minor", e.Minor). | |
| 122 | + | Uint64("seq", uint64(e.Seq)).Msg("unattributed X error") | |
| 123 | + | return | |
| 124 | + | } | |
| 125 | + | switch e.Kind { | |
| 126 | + | case kindRootMask: | |
| 127 | + | if e.Code == xproto.BadAccess { | |
| 128 | + | x.bootErr = ErrWMExists | |
| 129 | + | } | |
| 130 | + | case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus: | |
| 131 | + | // Usually BadWindow: the window died between request and reply and | |
| 132 | + | // DestroyNotify follows. Whatever the code, the awaited reply will | |
| 133 | + | // never come, so the map must not wait for it. | |
| 134 | + | s, rec := x.tab.live(e.Args[0]) | |
| 135 | + | if rec == nil { | |
| 136 | + | x.stats.Stale++ | |
| 137 | + | return | |
| 138 | + | } | |
| 139 | + | switch e.Kind { | |
| 140 | + | case kindProp: | |
| 141 | + | rec.pending &^= 1 << e.Args[1] | |
| 142 | + | case kindGeom: | |
| 143 | + | rec.pending &^= propGeom | |
| 144 | + | case kindClientIds: | |
| 145 | + | rec.pending &^= propClientIDs | |
| 146 | + | default: | |
| 147 | + | return | |
| 148 | + | } | |
| 149 | + | if e.Code != xproto.BadWindow && e.Code != xproto.BadDrawable { // a dead window is never announced | |
| 150 | + | x.checkReady(s) | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | // onCreateNotify records a new top-level window. Override-redirect windows | |
| 156 | + | // are kept (their geometry and serial matter later) but never reach the host. | |
| 157 | + | func (x *XWM) onCreateNotify(ev *xturbo.Event) { | |
| 158 | + | e := xproto.CreateNotifyOf(ev) | |
| 159 | + | xid := uint32(e.Window()) | |
| 160 | + | if xid == x.wmWin || x.tab.lookup(xid) != 0 { | |
| 161 | + | return | |
| 162 | + | } | |
| 163 | + | s := x.insert(xid) | |
| 164 | + | if s == 0 { | |
| 165 | + | return | |
| 166 | + | } | |
| 167 | + | r := x.tab.rec(s) | |
| 168 | + | r.override = e.OverrideRedirect() | |
| 169 | + | r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height()) | |
| 170 | + | if r.override { | |
| 171 | + | r.kind = kindUnmanaged | |
| 172 | + | x.stats.UnmanagedSeen++ | |
| 173 | + | return | |
| 174 | + | } | |
| 175 | + | if x.host != nil { | |
| 176 | + | x.adopt(s) | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // adopt announces a managed window to the host and starts watching it. | |
| 181 | + | func (x *XWM) adopt(s Slot) { | |
| 182 | + | r := x.tab.rec(s) | |
| 183 | + | r.id = x.host.Created(s, r.xid) | |
| 184 | + | var v xproto.ChangeWindowAttributesValues | |
| 185 | + | v.SetEventMask(xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange) | |
| 186 | + | x.expect(xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(r.xid), &v), kindAttr, x.tab.tag(s), 0) | |
| 187 | + | x.log.Debug().Uint32("xid", r.xid).Uint32("slot", uint32(s)).Msg("x11 window created") | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | func (x *XWM) onDestroyNotify(ev *xturbo.Event) { | |
| 191 | + | xid := uint32(xproto.DestroyNotifyOf(ev).Window()) | |
| 192 | + | s := x.tab.lookup(xid) | |
| 193 | + | if s == 0 { | |
| 194 | + | return | |
| 195 | + | } | |
| 196 | + | x.forgetWindow(s) | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // onTreeReply (boot): record every existing child and ask for its attributes. | |
| 200 | + | func (x *XWM) onTreeReply(r *xturbo.Reply) { | |
| 201 | + | q, ok := xproto.ParseQueryTreeReply(r.Raw) | |
| 202 | + | if !ok { | |
| 203 | + | return | |
| 204 | + | } | |
| 205 | + | children := q.Children() | |
| 206 | + | for i := 0; i < children.Len(); i++ { | |
| 207 | + | xid := uint32(children.At(i)) | |
| 208 | + | if xid == x.wmWin || x.tab.lookup(xid) != 0 { | |
| 209 | + | continue | |
| 210 | + | } | |
| 211 | + | s := x.insert(xid) | |
| 212 | + | if s == 0 { | |
| 213 | + | return | |
| 214 | + | } | |
| 215 | + | x.expect(xproto.GetWindowAttributes(x.conn.Buf(), xproto.Window(xid)), kindAttr, x.tab.tag(s), 0) | |
| 216 | + | } | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | // onAttrReply (boot): classify an existing window; viewable ones go through | |
| 220 | + | // the map flow once the host is attached. | |
| 221 | + | func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) { | |
| 222 | + | a, ok := xproto.ParseGetWindowAttributesReply(r.Raw) | |
| 223 | + | if !ok || x.host != nil { | |
| 224 | + | return | |
| 225 | + | } | |
| 226 | + | if a.OverrideRedirect() { | |
| 227 | + | rec.override = true | |
| 228 | + | rec.kind = kindUnmanaged | |
| 229 | + | x.stats.UnmanagedSeen++ | |
| 230 | + | return | |
| 231 | + | } | |
| 232 | + | if a.MapState() == xproto.MapStateViewable { | |
| 233 | + | rec.mapped, rec.mapping = true, true | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | func (x *XWM) onVersionReply(r *xturbo.Reply) { | |
| 238 | + | switch r.Args[0] { | |
| 239 | + | case versionComposite: | |
| 240 | + | if v, ok := composite.ParseQueryVersionReply(r.Raw); ok { | |
| 241 | + | x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("Composite") | |
| 242 | + | } | |
| 243 | + | case versionXFixes: | |
| 244 | + | if v, ok := xfixes.ParseQueryVersionReply(r.Raw); ok { | |
| 245 | + | x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("XFixes") | |
| 246 | + | } | |
| 247 | + | case versionRes: | |
| 248 | + | if v, ok := res.ParseQueryVersionReply(r.Raw); ok { | |
| 249 | + | x.log.Info().Uint16("major", v.ServerMajor()).Uint16("minor", v.ServerMinor()).Msg("X-Resource") | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | } | |
A internal/adapters/wayland/xwm/fdsource_mock_test.go +129 -0
| @@ -0,0 +1,129 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package xwm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // newMockfdSource creates a new instance of mockfdSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 12 | + | // The first argument is typically a *testing.T value. | |
| 13 | + | func newMockfdSource(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *mockfdSource { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | mock := &mockfdSource{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { | |
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 26 | + | helper.Helper() | |
| 27 | + | } | |
| 28 | + | mock.AssertExpectations(t) | |
| 29 | + | }) | |
| 30 | + | ||
| 31 | + | return mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // mockfdSource is an autogenerated mock type for the fdSource type | |
| 35 | + | type mockfdSource struct { | |
| 36 | + | mock.Mock | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | type mockfdSource_Expecter struct { | |
| 40 | + | mock *mock.Mock | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (_m *mockfdSource) EXPECT() *mockfdSource_Expecter { | |
| 44 | + | return &mockfdSource_Expecter{mock: &_m.Mock} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // Remove provides a mock function for the type mockfdSource | |
| 48 | + | func (_mock *mockfdSource) Remove() { | |
| 49 | + | _mock.Called() | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // mockfdSource_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' | |
| 54 | + | type mockfdSource_Remove_Call struct { | |
| 55 | + | *mock.Call | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // Remove is a helper method to define mock.On call | |
| 59 | + | func (_e *mockfdSource_Expecter) Remove() *mockfdSource_Remove_Call { | |
| 60 | + | return &mockfdSource_Remove_Call{Call: _e.mock.On("Remove")} | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func (_c *mockfdSource_Remove_Call) Run(run func()) *mockfdSource_Remove_Call { | |
| 64 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 65 | + | run() | |
| 66 | + | }) | |
| 67 | + | return _c | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (_c *mockfdSource_Remove_Call) Return() *mockfdSource_Remove_Call { | |
| 71 | + | _c.Call.Return() | |
| 72 | + | return _c | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (_c *mockfdSource_Remove_Call) RunAndReturn(run func()) *mockfdSource_Remove_Call { | |
| 76 | + | _c.Run(run) | |
| 77 | + | return _c | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // SetWritable provides a mock function for the type mockfdSource | |
| 81 | + | func (_mock *mockfdSource) SetWritable(on bool) error { | |
| 82 | + | ret := _mock.Called(on) | |
| 83 | + | ||
| 84 | + | if len(ret) == 0 { | |
| 85 | + | panic("no return value specified for SetWritable") | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | var r0 error | |
| 89 | + | if returnFunc, ok := ret.Get(0).(func(bool) error); ok { | |
| 90 | + | r0 = returnFunc(on) | |
| 91 | + | } else { | |
| 92 | + | r0 = ret.Error(0) | |
| 93 | + | } | |
| 94 | + | return r0 | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // mockfdSource_SetWritable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWritable' | |
| 98 | + | type mockfdSource_SetWritable_Call struct { | |
| 99 | + | *mock.Call | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // SetWritable is a helper method to define mock.On call | |
| 103 | + | // - on bool | |
| 104 | + | func (_e *mockfdSource_Expecter) SetWritable(on any) *mockfdSource_SetWritable_Call { | |
| 105 | + | return &mockfdSource_SetWritable_Call{Call: _e.mock.On("SetWritable", on)} | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func (_c *mockfdSource_SetWritable_Call) Run(run func(on bool)) *mockfdSource_SetWritable_Call { | |
| 109 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 110 | + | var arg0 bool | |
| 111 | + | if args[0] != nil { | |
| 112 | + | arg0 = args[0].(bool) | |
| 113 | + | } | |
| 114 | + | run( | |
| 115 | + | arg0, | |
| 116 | + | ) | |
| 117 | + | }) | |
| 118 | + | return _c | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func (_c *mockfdSource_SetWritable_Call) Return(err error) *mockfdSource_SetWritable_Call { | |
| 122 | + | _c.Call.Return(err) | |
| 123 | + | return _c | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func (_c *mockfdSource_SetWritable_Call) RunAndReturn(run func(on bool) error) *mockfdSource_SetWritable_Call { | |
| 127 | + | _c.Call.Return(run) | |
| 128 | + | return _c | |
| 129 | + | } | |
A internal/adapters/wayland/xwm/fixes_test.go +230 -0
| @@ -0,0 +1,230 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "bnema.dev/xturbo" | |
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | "github.com/stretchr/testify/mock" | |
| 11 | + | "github.com/stretchr/testify/require" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | func getProps(p *xpeer, skip int, win uint32) int { | |
| 15 | + | n := 0 | |
| 16 | + | for _, r := range since(p, skip, opGetProperty) { | |
| 17 | + | if r.u32(4) == win { | |
| 18 | + | n++ | |
| 19 | + | } | |
| 20 | + | } | |
| 21 | + | return n | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // Our own _NET_WM_STATE writes must not be read back once the window is ready. | |
| 25 | + | func TestOwnNetWMStateWriteIsNotReadBack(t *testing.T) { | |
| 26 | + | hs := newHarness(t, nil) | |
| 27 | + | const win = 0x500001 | |
| 28 | + | hs.classic(win) | |
| 29 | + | s := hs.mapAndWait(win) | |
| 30 | + | ||
| 31 | + | hs.x.Configure(s, ports.ConfigureWindow{Width: 300, Height: 200, Fullscreen: true}) | |
| 32 | + | n := settle(t, hs.x, hs.p) | |
| 33 | + | require.True(t, hs.x.tab.rec(s).fullscreen, "the written state is the known state") | |
| 34 | + | st, _ := hs.p.prop(win, "_NET_WM_STATE") | |
| 35 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FULLSCREEN")}, atomsOf(st)) | |
| 36 | + | ||
| 37 | + | ev := hs.x.Stats().Events | |
| 38 | + | hs.p.propertyNotify(win, hs.p.atom("_NET_WM_STATE"), false) | |
| 39 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > ev }) | |
| 40 | + | settle(t, hs.x, hs.p) | |
| 41 | + | require.Zero(t, getProps(hs.p, n, win), "no GetProperty") | |
| 42 | + | ||
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // A pre-map XMoveResize is honoured, floating or not. | |
| 46 | + | func TestConfigureRequestBeforeReadyIsHonoured(t *testing.T) { | |
| 47 | + | hs := newHarness(t, nil) | |
| 48 | + | const win = 0x500001 | |
| 49 | + | hs.classic(win) | |
| 50 | + | hs.create(win) | |
| 51 | + | n := settle(t, hs.x, hs.p) | |
| 52 | + | ||
| 53 | + | mask := uint16(xproto.ConfigWindowX | xproto.ConfigWindowY | xproto.ConfigWindowWidth | xproto.ConfigWindowHeight) | |
| 54 | + | hs.x.feed(t, hs.p, func() { sendConfigureRequest(hs.p, win, mask, 11, 22, 640, 70000&0xffff) }) | |
| 55 | + | settle(t, hs.x, hs.p) | |
| 56 | + | cw := since(hs.p, n, opConfigureWindow) | |
| 57 | + | require.Len(t, cw, 1) | |
| 58 | + | require.Equal(t, []uint32{11, 22, 640, 4464}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24)}) | |
| 59 | + | require.Len(t, sentEvents(hs.p, n, win), 1) | |
| 60 | + | ||
| 61 | + | hs.expectReady(win) | |
| 62 | + | hs.p.mapRequest(win) | |
| 63 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 64 | + | require.Equal(t, 640, hs.ready[win].Width) | |
| 65 | + | require.Equal(t, 4464, hs.ready[win].Height) | |
| 66 | + | require.False(t, hs.ready[win].Floating) | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | // After a remap the state is rewritten from scratch, FOCUSED included. | |
| 70 | + | func TestRemapRewritesStateWithFocused(t *testing.T) { | |
| 71 | + | hs := newHarness(t, nil) | |
| 72 | + | const win = 0x500001 | |
| 73 | + | hs.classic(win) | |
| 74 | + | s := hs.mapAndWait(win) | |
| 75 | + | hs.x.Focus(s) | |
| 76 | + | settle(t, hs.x, hs.p) | |
| 77 | + | st, _ := hs.p.prop(win, "_NET_WM_STATE") | |
| 78 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 79 | + | ||
| 80 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |
| 81 | + | hs.p.unmapNotify(win, false) | |
| 82 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping }) | |
| 83 | + | delete(hs.ready, win) | |
| 84 | + | hs.expectReady(win) | |
| 85 | + | hs.p.mapRequest(win) | |
| 86 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 87 | + | ||
| 88 | + | hs.x.Focus(s) | |
| 89 | + | settle(t, hs.x, hs.p) | |
| 90 | + | st, _ = hs.p.prop(win, "_NET_WM_STATE") | |
| 91 | + | require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st)) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // Replies of a previous map never satisfy the next one (xturbo usually retires | |
| 95 | + | // their slots first; the generation guards the rest). | |
| 96 | + | func TestStaleRepliesOfPreviousMapAreDropped(t *testing.T) { | |
| 97 | + | hs := newHarness(t, nil) | |
| 98 | + | const win = 0x500001 | |
| 99 | + | hs.classic(win) | |
| 100 | + | s := hs.create(win) | |
| 101 | + | hs.p.holdReplies() // the second map is never answered | |
| 102 | + | hs.p.mapRequest(win) | |
| 103 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping }) | |
| 104 | + | oldTag := hs.x.tab.tag(s) | |
| 105 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |
| 106 | + | hs.p.unmapNotify(win, false) | |
| 107 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping }) | |
| 108 | + | hs.p.mapRequest(win) | |
| 109 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping }) | |
| 110 | + | require.NotEqual(t, oldTag, hs.x.tab.tag(s)) | |
| 111 | + | ||
| 112 | + | rep := make([]byte, 32) | |
| 113 | + | rep[0] = 1 | |
| 114 | + | for idx := 0; idx < propCount; idx++ { | |
| 115 | + | hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{oldTag, uint32(idx)}, Raw: rep}) | |
| 116 | + | } | |
| 117 | + | hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{oldTag}, Raw: rep}) | |
| 118 | + | require.EqualValues(t, propCount+1, hs.x.Stats().Stale) | |
| 119 | + | require.False(t, hs.x.tab.rec(s).ready) | |
| 120 | + | require.Equal(t, propMaskAll&^propClientIDs, hs.x.tab.rec(s).pending, "the new map still awaits its own replies") | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func TestGetGeometryBadDrawableIsNotReady(t *testing.T) { | |
| 124 | + | hs := newHarness(t, nil) | |
| 125 | + | const win = 0x500001 | |
| 126 | + | hs.classic(win) | |
| 127 | + | s := hs.create(win) | |
| 128 | + | hs.p.set(func(p *xpeer) { p.bad[win] = true }) | |
| 129 | + | hs.p.mapRequest(win) | |
| 130 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 }) | |
| 131 | + | require.Zero(t, hs.x.Stats().Unattributed) | |
| 132 | + | require.False(t, hs.x.tab.rec(s).ready) | |
| 133 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 134 | + | hs.p.destroyNotify(win) | |
| 135 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // A host that refocuses the window while dissociating it must not leave a | |
| 139 | + | // dangling active slot. | |
| 140 | + | func TestForgetWindowClearsActiveSetByHost(t *testing.T) { | |
| 141 | + | x, p, h, _, _ := attached(t) | |
| 142 | + | const win = 0x500001 | |
| 143 | + | s := newWin(t, x, p, h, win) | |
| 144 | + | h.EXPECT().Dissociate(s, true).Run(func(sl Slot, _ bool) { x.Focus(sl) }).Once() | |
| 145 | + | p.destroyNotify(win) | |
| 146 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 }) | |
| 147 | + | require.Zero(t, x.active) | |
| 148 | + | require.False(t, x.refocus) | |
| 149 | + | settle(t, x, p) | |
| 150 | + | act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 151 | + | require.Equal(t, []uint32{0}, atomsOf(act)) | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | func TestTitleAndAppIDAreValidUTF8(t *testing.T) { | |
| 155 | + | hs := newHarness(t, nil) | |
| 156 | + | const win = 0x500001 | |
| 157 | + | hs.classic(win) | |
| 158 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |
| 159 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("caf\xe9")) | |
| 160 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00C\xe9\x00")) | |
| 161 | + | s := hs.mapAndWait(win) | |
| 162 | + | require.Equal(t, "caf\uFFFD", hs.ready[win].Title) | |
| 163 | + | require.Equal(t, "C\uFFFD", hs.ready[win].AppID) | |
| 164 | + | ||
| 165 | + | // Unchanged after sanitising: no host call. | |
| 166 | + | hs.notifyWMName(win) | |
| 167 | + | // A changed truncated multibyte title reaches the host sanitised. | |
| 168 | + | hs.h.EXPECT().Title(s, "\u00e9\uFFFD").Once() | |
| 169 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("\xc3\xa9\xe2\x82")) | |
| 170 | + | hs.notifyWMName(win) | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func TestSyntheticConfigureNotifyClampsCoordinates(t *testing.T) { | |
| 174 | + | x, p, h, _, _ := attached(t) | |
| 175 | + | const win = 0x500001 | |
| 176 | + | s := newWin(t, x, p, h, win) | |
| 177 | + | n := settle(t, x, p) | |
| 178 | + | r := x.tab.rec(s) | |
| 179 | + | r.x, r.y, r.w, r.h = 70000, -70000, 70000, 5 | |
| 180 | + | x.sendConfigureNotify(r) | |
| 181 | + | settle(t, x, p) | |
| 182 | + | ev := sentEvents(p, n, win) | |
| 183 | + | require.Len(t, ev, 1) | |
| 184 | + | require.EqualValues(t, 32767, int16(binary.LittleEndian.Uint16(ev[0][16:]))) | |
| 185 | + | require.EqualValues(t, -32768, int16(binary.LittleEndian.Uint16(ev[0][18:]))) | |
| 186 | + | require.EqualValues(t, 65535, binary.LittleEndian.Uint16(ev[0][20:])) | |
| 187 | + | require.EqualValues(t, 5, binary.LittleEndian.Uint16(ev[0][22:])) | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // A window withdrawn before Attach is not mapped by Attach; a pre-attach | |
| 191 | + | // ConfigureRequest is honoured. | |
| 192 | + | func TestUnmapAndConfigureBeforeAttach(t *testing.T) { | |
| 193 | + | x, p := bootPeer(t, nil) | |
| 194 | + | const gone, sized = 0x500001, 0x500002 | |
| 195 | + | for _, w := range []uint32{gone, sized} { | |
| 196 | + | p.setProp(w, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00")) | |
| 197 | + | p.geoms[w] = xpeerGeom{w: 300, h: 200} | |
| 198 | + | p.createNotify(w, false) | |
| 199 | + | p.mapRequest(w) | |
| 200 | + | } | |
| 201 | + | p.unmapNotify(gone, false) | |
| 202 | + | mask := uint16(xproto.ConfigWindowWidth | xproto.ConfigWindowHeight) | |
| 203 | + | sendConfigureRequest(p, sized, mask, 0, 0, 500, 400) | |
| 204 | + | readUntil(t, x, func() bool { return x.Stats().Events >= 6 }) | |
| 205 | + | n := len(p.requests()) | |
| 206 | + | ||
| 207 | + | h := newMockhost(t) | |
| 208 | + | src := newMockfdSource(t) | |
| 209 | + | src.EXPECT().Remove().Maybe() | |
| 210 | + | post, _ := newPost() | |
| 211 | + | var got WindowInfo | |
| 212 | + | done := false | |
| 213 | + | h.EXPECT().Created(mock.Anything, mock.Anything).Return(ports.WindowID(1)).Twice() | |
| 214 | + | h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once() | |
| 215 | + | x.attach(src, h, post) | |
| 216 | + | readUntil(t, x, func() bool { return done }) | |
| 217 | + | settle(t, x, p) | |
| 218 | + | for _, r := range since(p, n, opMapWindow) { | |
| 219 | + | require.NotEqual(t, uint32(gone), r.u32(4), "withdrawn window must not be mapped") | |
| 220 | + | } | |
| 221 | + | require.Equal(t, 500, got.Width) | |
| 222 | + | require.Equal(t, 400, got.Height) | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | func (hs *harness) notifyWMName(win uint32) { | |
| 226 | + | hs.t.Helper() | |
| 227 | + | before := hs.x.Stats().Replies | |
| 228 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false) | |
| 229 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 230 | + | } | |
A internal/adapters/wayland/xwm/focus.go +203 -0
| @@ -0,0 +1,203 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "slices" | |
| 6 | + | ||
| 7 | + | "bnema.dev/xturbo" | |
| 8 | + | "bnema.dev/xturbo/xproto" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Bits of XWM.listsDirty: the client lists to rewrite at flush. | |
| 12 | + | const ( | |
| 13 | + | listClients uint8 = 1 << iota | |
| 14 | + | listStacking | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // onFocusIn denies a focus change the XWM did not make: Wayland is the truth. | |
| 18 | + | // The active window's focus is re-issued once at the next flush. | |
| 19 | + | func (x *XWM) onFocusIn(ev *xturbo.Event) { | |
| 20 | + | e := xproto.FocusInOf(ev) | |
| 21 | + | if e.Detail() == xproto.NotifyDetailPointer { | |
| 22 | + | return | |
| 23 | + | } | |
| 24 | + | s := x.tab.lookup(uint32(e.Event())) | |
| 25 | + | if s == 0 || s == x.active { | |
| 26 | + | return | |
| 27 | + | } | |
| 28 | + | x.refocus = true | |
| 29 | + | x.wake() | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // Focus gives keyboard focus to s; 0 means a native Wayland window or none. | |
| 33 | + | // Focus never uses PointerRoot: without an X window to focus it is None. | |
| 34 | + | func (x *XWM) Focus(s Slot) { | |
| 35 | + | if x.closed { | |
| 36 | + | return | |
| 37 | + | } | |
| 38 | + | r := x.managedRec(s) | |
| 39 | + | if r == nil { | |
| 40 | + | s = 0 | |
| 41 | + | } | |
| 42 | + | if s == x.active { | |
| 43 | + | return | |
| 44 | + | } | |
| 45 | + | if old := x.managedRec(x.active); old != nil { | |
| 46 | + | old.pend.activated = false | |
| 47 | + | x.markDirty(x.active, old) | |
| 48 | + | } | |
| 49 | + | x.active = s | |
| 50 | + | if r == nil { | |
| 51 | + | x.issueFocus(0, nil) | |
| 52 | + | x.setActive(0) | |
| 53 | + | x.wake() | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | r.pend.activated = true | |
| 57 | + | x.markDirty(s, r) | |
| 58 | + | x.issueFocus(s, r) | |
| 59 | + | x.setActive(r.xid) | |
| 60 | + | x.raise(r) | |
| 61 | + | x.wake() | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // issueFocus applies the ICCCM input model of r: Passive and Locally active | |
| 65 | + | // windows get SetInputFocus, Locally and Globally active ones WM_TAKE_FOCUS, | |
| 66 | + | // No-input windows nothing. A nil r clears the X focus. | |
| 67 | + | func (x *XWM) issueFocus(s Slot, r *record) { | |
| 68 | + | b := x.conn.Buf() | |
| 69 | + | if r == nil { | |
| 70 | + | xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0) | |
| 71 | + | return | |
| 72 | + | } | |
| 73 | + | if r.input { | |
| 74 | + | x.expect(xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.Window(r.xid), 0), kindFocus, x.tab.tag(s), 0) | |
| 75 | + | } | |
| 76 | + | if r.takeFocus { | |
| 77 | + | x.sendProtocol(r.xid, atomWMTakeFocus) | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // sendProtocol sends a WM_PROTOCOLS client message [proto, CurrentTime]. | |
| 82 | + | func (x *XWM) sendProtocol(xid uint32, proto int) { | |
| 83 | + | var raw [32]byte | |
| 84 | + | raw[0], raw[1] = 33, 32 // ClientMessage, format 32 | |
| 85 | + | binary.LittleEndian.PutUint32(raw[4:], xid) | |
| 86 | + | binary.LittleEndian.PutUint32(raw[8:], x.atoms[atomWMProtocols]) | |
| 87 | + | binary.LittleEndian.PutUint32(raw[12:], x.atoms[proto]) | |
| 88 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(xid), xproto.EventMaskNoEvent, &raw) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // setActive writes _NET_ACTIVE_WINDOW on the root. | |
| 92 | + | func (x *XWM) setActive(xid uint32) { | |
| 93 | + | v := [1]uint32{xid} | |
| 94 | + | x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, v[:]) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // raise restacks r above its siblings and at the top of the stacking list. | |
| 98 | + | func (x *XWM) raise(r *record) { | |
| 99 | + | var v xproto.ConfigureWindowValues | |
| 100 | + | v.SetStackMode(xproto.StackModeAbove) | |
| 101 | + | xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v) | |
| 102 | + | if i := slices.Index(x.stacking, r.xid); i >= 0 { | |
| 103 | + | x.stacking = append(x.stacking[:i], x.stacking[i+1:]...) | |
| 104 | + | x.stacking = append(x.stacking, r.xid) | |
| 105 | + | x.listsDirty |= listStacking | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client. | |
| 110 | + | func (x *XWM) CloseWindow(s Slot) { | |
| 111 | + | r := x.managedRec(s) | |
| 112 | + | if r == nil { | |
| 113 | + | return | |
| 114 | + | } | |
| 115 | + | if r.deleteWindow { | |
| 116 | + | x.sendProtocol(r.xid, atomWMDeleteWindow) | |
| 117 | + | } else { | |
| 118 | + | xproto.KillClient(x.conn.Buf(), r.xid) | |
| 119 | + | } | |
| 120 | + | x.wake() | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // addClient puts a ready window at the end of both client lists. | |
| 124 | + | func (x *XWM) addClient(xid uint32) { | |
| 125 | + | if !slices.Contains(x.clients, xid) { | |
| 126 | + | x.clients = append(x.clients, xid) | |
| 127 | + | x.listsDirty |= listClients | |
| 128 | + | } | |
| 129 | + | if !slices.Contains(x.stacking, xid) { | |
| 130 | + | x.stacking = append(x.stacking, xid) | |
| 131 | + | x.listsDirty |= listStacking | |
| 132 | + | } | |
| 133 | + | x.wake() | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // removeClient drops an unmapped or destroyed window from both client lists. | |
| 137 | + | func (x *XWM) removeClient(xid uint32) { | |
| 138 | + | if i := slices.Index(x.clients, xid); i >= 0 { | |
| 139 | + | x.clients = slices.Delete(x.clients, i, i+1) | |
| 140 | + | x.listsDirty |= listClients | |
| 141 | + | } | |
| 142 | + | if i := slices.Index(x.stacking, xid); i >= 0 { | |
| 143 | + | x.stacking = slices.Delete(x.stacking, i, i+1) | |
| 144 | + | x.listsDirty |= listStacking | |
| 145 | + | } | |
| 146 | + | x.wake() | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // dropWindow takes s out of the client lists, the focus and the configure | |
| 150 | + | // queue when it stops being mapped (unmap, destroy). It is idempotent. | |
| 151 | + | func (x *XWM) dropWindow(s Slot) { | |
| 152 | + | r := x.tab.rec(s) | |
| 153 | + | if r == nil || r.kind != kindManaged { | |
| 154 | + | return | |
| 155 | + | } | |
| 156 | + | x.removeClient(r.xid) | |
| 157 | + | r.pend.dirty = false // the queued slot is skipped at flush | |
| 158 | + | // A remap starts from a clean _NET_WM_STATE, so Focus writes FOCUSED again. | |
| 159 | + | r.pend.activated, r.sent.activated = false, false | |
| 160 | + | r.sent.fullscreen, r.stateSent = false, 0 | |
| 161 | + | if x.active == s { | |
| 162 | + | x.active = 0 | |
| 163 | + | x.refocus = false | |
| 164 | + | x.setActive(0) | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // forgetWindow ends the life of s (destroy, reparent away): it is dropped, | |
| 169 | + | // the host is told if it knew the window, and the record is freed. | |
| 170 | + | func (x *XWM) forgetWindow(s Slot) { | |
| 171 | + | r := x.tab.rec(s) | |
| 172 | + | if r == nil || r.kind == kindFree { | |
| 173 | + | return | |
| 174 | + | } | |
| 175 | + | x.dropWindow(s) | |
| 176 | + | if r.kind == kindManaged && r.id != 0 && x.host != nil { | |
| 177 | + | r.id = 0 | |
| 178 | + | x.host.Dissociate(s, true) | |
| 179 | + | if x.active == s { // the host re-focused the window while dissociating it | |
| 180 | + | x.active, x.refocus = 0, false | |
| 181 | + | x.issueFocus(0, nil) | |
| 182 | + | x.setActive(0) | |
| 183 | + | x.wake() | |
| 184 | + | } | |
| 185 | + | } | |
| 186 | + | x.tab.remove(s) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | // flushFocus rewrites the client lists that changed and re-issues a denied | |
| 190 | + | // focus, once per display iteration. | |
| 191 | + | func (x *XWM) flushFocus() { | |
| 192 | + | if x.listsDirty&listClients != 0 { | |
| 193 | + | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients) | |
| 194 | + | } | |
| 195 | + | if x.listsDirty&listStacking != 0 { | |
| 196 | + | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking) | |
| 197 | + | } | |
| 198 | + | x.listsDirty = 0 | |
| 199 | + | if x.refocus { | |
| 200 | + | x.refocus = false | |
| 201 | + | x.issueFocus(x.active, x.managedRec(x.active)) | |
| 202 | + | } | |
| 203 | + | } | |
A internal/adapters/wayland/xwm/host.go +49 -0
| @@ -0,0 +1,49 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/neferwl/internal/ports" | |
| 4 | + | ||
| 5 | + | // Slot is the XWM's window slot: table index + 1, stable until the window is | |
| 6 | + | // destroyed. 0 means none. | |
| 7 | + | type Slot uint32 | |
| 8 | + | ||
| 9 | + | // WindowInfo is what the XWM learned about a managed window by the time it is | |
| 10 | + | // ready to be mapped. The host copies it. | |
| 11 | + | type WindowInfo struct { | |
| 12 | + | PID int | |
| 13 | + | Floating bool // dialog/utility/splash/fixed size | |
| 14 | + | Width, Height int // requested size, logical (scale 1) | |
| 15 | + | Parent Slot | |
| 16 | + | Modal bool // _NET_WM_STATE_MODAL | |
| 17 | + | Fullscreen bool // _NET_WM_STATE_FULLSCREEN at map | |
| 18 | + | Title, AppID string | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | // host is the wayland server as the XWM sees it. Display goroutine only. | |
| 22 | + | // All strings passed are owned by the XWM record and already deduplicated | |
| 23 | + | // (only passed when changed). | |
| 24 | + | type host interface { | |
| 25 | + | // Created: a managed (non-override-redirect) X window is known; the host | |
| 26 | + | // allocates a ports.WindowID and returns it. Not yet mapped. | |
| 27 | + | Created(slot Slot, xid uint32) ports.WindowID | |
| 28 | + | // Ready: properties read and MapWindow issued; the surface may still be | |
| 29 | + | // missing. Info is copied by the host. | |
| 30 | + | Ready(slot Slot, info WindowInfo) | |
| 31 | + | // Associate links the X window to the wl_surface with identity surfaceID. | |
| 32 | + | // The host maps the window on the surface's first buffer commit. | |
| 33 | + | Associate(slot Slot, surfaceID uint64) | |
| 34 | + | // Dissociate: UnmapNotify/DestroyNotify; the host emits WindowUnmapped if | |
| 35 | + | // mapped and drops the surface link. destroyed also frees the WindowID. | |
| 36 | + | Dissociate(slot Slot, destroyed bool) | |
| 37 | + | Title(slot Slot, title string) // changed only | |
| 38 | + | AppID(slot Slot, appID string) // changed only (WM_CLASS class) | |
| 39 | + | Parent(slot Slot, parent Slot) // 0 = none; after a WM_TRANSIENT_FOR change | |
| 40 | + | // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message | |
| 41 | + | // or initial state); the host emits WindowFullscreenRequest. | |
| 42 | + | Fullscreen(slot Slot, on bool) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // fdSource is the display's event source for the X socket (*server.Source). | |
| 46 | + | type fdSource interface { | |
| 47 | + | SetWritable(on bool) error | |
| 48 | + | Remove() | |
| 49 | + | } | |
A internal/adapters/wayland/xwm/host_mock_test.go +425 -0
| @@ -0,0 +1,425 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package xwm | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | mock "github.com/stretchr/testify/mock" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // newMockhost creates a new instance of mockhost. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 13 | + | // The first argument is typically a *testing.T value. | |
| 14 | + | func newMockhost(t interface { | |
| 15 | + | mock.TestingT | |
| 16 | + | Cleanup(func()) | |
| 17 | + | }) *mockhost { | |
| 18 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 19 | + | helper.Helper() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | mock := &mockhost{} | |
| 23 | + | mock.Mock.Test(t) | |
| 24 | + | ||
| 25 | + | t.Cleanup(func() { | |
| 26 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 27 | + | helper.Helper() | |
| 28 | + | } | |
| 29 | + | mock.AssertExpectations(t) | |
| 30 | + | }) | |
| 31 | + | ||
| 32 | + | return mock | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // mockhost is an autogenerated mock type for the host type | |
| 36 | + | type mockhost struct { | |
| 37 | + | mock.Mock | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | type mockhost_Expecter struct { | |
| 41 | + | mock *mock.Mock | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | func (_m *mockhost) EXPECT() *mockhost_Expecter { | |
| 45 | + | return &mockhost_Expecter{mock: &_m.Mock} | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | // AppID provides a mock function for the type mockhost | |
| 49 | + | func (_mock *mockhost) AppID(slot Slot, appID string) { | |
| 50 | + | _mock.Called(slot, appID) | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // mockhost_AppID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppID' | |
| 55 | + | type mockhost_AppID_Call struct { | |
| 56 | + | *mock.Call | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // AppID is a helper method to define mock.On call | |
| 60 | + | // - slot Slot | |
| 61 | + | // - appID string | |
| 62 | + | func (_e *mockhost_Expecter) AppID(slot any, appID any) *mockhost_AppID_Call { | |
| 63 | + | return &mockhost_AppID_Call{Call: _e.mock.On("AppID", slot, appID)} | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func (_c *mockhost_AppID_Call) Run(run func(slot Slot, appID string)) *mockhost_AppID_Call { | |
| 67 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 68 | + | var arg0 Slot | |
| 69 | + | if args[0] != nil { | |
| 70 | + | arg0 = args[0].(Slot) | |
| 71 | + | } | |
| 72 | + | var arg1 string | |
| 73 | + | if args[1] != nil { | |
| 74 | + | arg1 = args[1].(string) | |
| 75 | + | } | |
| 76 | + | run( | |
| 77 | + | arg0, | |
| 78 | + | arg1, | |
| 79 | + | ) | |
| 80 | + | }) | |
| 81 | + | return _c | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | func (_c *mockhost_AppID_Call) Return() *mockhost_AppID_Call { | |
| 85 | + | _c.Call.Return() | |
| 86 | + | return _c | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (_c *mockhost_AppID_Call) RunAndReturn(run func(slot Slot, appID string)) *mockhost_AppID_Call { | |
| 90 | + | _c.Run(run) | |
| 91 | + | return _c | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // Associate provides a mock function for the type mockhost | |
| 95 | + | func (_mock *mockhost) Associate(slot Slot, surfaceID uint64) { | |
| 96 | + | _mock.Called(slot, surfaceID) | |
| 97 | + | return | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // mockhost_Associate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Associate' | |
| 101 | + | type mockhost_Associate_Call struct { | |
| 102 | + | *mock.Call | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // Associate is a helper method to define mock.On call | |
| 106 | + | // - slot Slot | |
| 107 | + | // - surfaceID uint64 | |
| 108 | + | func (_e *mockhost_Expecter) Associate(slot any, surfaceID any) *mockhost_Associate_Call { | |
| 109 | + | return &mockhost_Associate_Call{Call: _e.mock.On("Associate", slot, surfaceID)} | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | func (_c *mockhost_Associate_Call) Run(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call { | |
| 113 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 114 | + | var arg0 Slot | |
| 115 | + | if args[0] != nil { | |
| 116 | + | arg0 = args[0].(Slot) | |
| 117 | + | } | |
| 118 | + | var arg1 uint64 | |
| 119 | + | if args[1] != nil { | |
| 120 | + | arg1 = args[1].(uint64) | |
| 121 | + | } | |
| 122 | + | run( | |
| 123 | + | arg0, | |
| 124 | + | arg1, | |
| 125 | + | ) | |
| 126 | + | }) | |
| 127 | + | return _c | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | func (_c *mockhost_Associate_Call) Return() *mockhost_Associate_Call { | |
| 131 | + | _c.Call.Return() | |
| 132 | + | return _c | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func (_c *mockhost_Associate_Call) RunAndReturn(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call { | |
| 136 | + | _c.Run(run) | |
| 137 | + | return _c | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | // Created provides a mock function for the type mockhost | |
| 141 | + | func (_mock *mockhost) Created(slot Slot, xid uint32) ports.WindowID { | |
| 142 | + | ret := _mock.Called(slot, xid) | |
| 143 | + | ||
| 144 | + | if len(ret) == 0 { | |
| 145 | + | panic("no return value specified for Created") | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | var r0 ports.WindowID | |
| 149 | + | if returnFunc, ok := ret.Get(0).(func(Slot, uint32) ports.WindowID); ok { | |
| 150 | + | r0 = returnFunc(slot, xid) | |
| 151 | + | } else { | |
| 152 | + | r0 = ret.Get(0).(ports.WindowID) | |
| 153 | + | } | |
| 154 | + | return r0 | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | // mockhost_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created' | |
| 158 | + | type mockhost_Created_Call struct { | |
| 159 | + | *mock.Call | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // Created is a helper method to define mock.On call | |
| 163 | + | // - slot Slot | |
| 164 | + | // - xid uint32 | |
| 165 | + | func (_e *mockhost_Expecter) Created(slot any, xid any) *mockhost_Created_Call { | |
| 166 | + | return &mockhost_Created_Call{Call: _e.mock.On("Created", slot, xid)} | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | func (_c *mockhost_Created_Call) Run(run func(slot Slot, xid uint32)) *mockhost_Created_Call { | |
| 170 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 171 | + | var arg0 Slot | |
| 172 | + | if args[0] != nil { | |
| 173 | + | arg0 = args[0].(Slot) | |
| 174 | + | } | |
| 175 | + | var arg1 uint32 | |
| 176 | + | if args[1] != nil { | |
| 177 | + | arg1 = args[1].(uint32) | |
| 178 | + | } | |
| 179 | + | run( | |
| 180 | + | arg0, | |
| 181 | + | arg1, | |
| 182 | + | ) | |
| 183 | + | }) | |
| 184 | + | return _c | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | func (_c *mockhost_Created_Call) Return(windowID ports.WindowID) *mockhost_Created_Call { | |
| 188 | + | _c.Call.Return(windowID) | |
| 189 | + | return _c | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | func (_c *mockhost_Created_Call) RunAndReturn(run func(slot Slot, xid uint32) ports.WindowID) *mockhost_Created_Call { | |
| 193 | + | _c.Call.Return(run) | |
| 194 | + | return _c | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | // Dissociate provides a mock function for the type mockhost | |
| 198 | + | func (_mock *mockhost) Dissociate(slot Slot, destroyed bool) { | |
| 199 | + | _mock.Called(slot, destroyed) | |
| 200 | + | return | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // mockhost_Dissociate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dissociate' | |
| 204 | + | type mockhost_Dissociate_Call struct { | |
| 205 | + | *mock.Call | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | // Dissociate is a helper method to define mock.On call | |
| 209 | + | // - slot Slot | |
| 210 | + | // - destroyed bool | |
| 211 | + | func (_e *mockhost_Expecter) Dissociate(slot any, destroyed any) *mockhost_Dissociate_Call { | |
| 212 | + | return &mockhost_Dissociate_Call{Call: _e.mock.On("Dissociate", slot, destroyed)} | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | func (_c *mockhost_Dissociate_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call { | |
| 216 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 217 | + | var arg0 Slot | |
| 218 | + | if args[0] != nil { | |
| 219 | + | arg0 = args[0].(Slot) | |
| 220 | + | } | |
| 221 | + | var arg1 bool | |
| 222 | + | if args[1] != nil { | |
| 223 | + | arg1 = args[1].(bool) | |
| 224 | + | } | |
| 225 | + | run( | |
| 226 | + | arg0, | |
| 227 | + | arg1, | |
| 228 | + | ) | |
| 229 | + | }) | |
| 230 | + | return _c | |
| 231 | + | } | |
| 232 | + | ||
| 233 | + | func (_c *mockhost_Dissociate_Call) Return() *mockhost_Dissociate_Call { | |
| 234 | + | _c.Call.Return() | |
| 235 | + | return _c | |
| 236 | + | } | |
| 237 | + | ||
| 238 | + | func (_c *mockhost_Dissociate_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call { | |
| 239 | + | _c.Run(run) | |
| 240 | + | return _c | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | // Fullscreen provides a mock function for the type mockhost | |
| 244 | + | func (_mock *mockhost) Fullscreen(slot Slot, on bool) { | |
| 245 | + | _mock.Called(slot, on) | |
| 246 | + | return | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // mockhost_Fullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fullscreen' | |
| 250 | + | type mockhost_Fullscreen_Call struct { | |
| 251 | + | *mock.Call | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | // Fullscreen is a helper method to define mock.On call | |
| 255 | + | // - slot Slot | |
| 256 | + | // - on bool | |
| 257 | + | func (_e *mockhost_Expecter) Fullscreen(slot any, on any) *mockhost_Fullscreen_Call { | |
| 258 | + | return &mockhost_Fullscreen_Call{Call: _e.mock.On("Fullscreen", slot, on)} | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | func (_c *mockhost_Fullscreen_Call) Run(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call { | |
| 262 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 263 | + | var arg0 Slot | |
| 264 | + | if args[0] != nil { | |
| 265 | + | arg0 = args[0].(Slot) | |
| 266 | + | } | |
| 267 | + | var arg1 bool | |
| 268 | + | if args[1] != nil { | |
| 269 | + | arg1 = args[1].(bool) | |
| 270 | + | } | |
| 271 | + | run( | |
| 272 | + | arg0, | |
| 273 | + | arg1, | |
| 274 | + | ) | |
| 275 | + | }) | |
| 276 | + | return _c | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | func (_c *mockhost_Fullscreen_Call) Return() *mockhost_Fullscreen_Call { | |
| 280 | + | _c.Call.Return() | |
| 281 | + | return _c | |
| 282 | + | } | |
| 283 | + | ||
| 284 | + | func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call { | |
| 285 | + | _c.Run(run) | |
| 286 | + | return _c | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | // Parent provides a mock function for the type mockhost | |
| 290 | + | func (_mock *mockhost) Parent(slot Slot, parent Slot) { | |
| 291 | + | _mock.Called(slot, parent) | |
| 292 | + | return | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | // mockhost_Parent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parent' | |
| 296 | + | type mockhost_Parent_Call struct { | |
| 297 | + | *mock.Call | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | // Parent is a helper method to define mock.On call | |
| 301 | + | // - slot Slot | |
| 302 | + | // - parent Slot | |
| 303 | + | func (_e *mockhost_Expecter) Parent(slot any, parent any) *mockhost_Parent_Call { | |
| 304 | + | return &mockhost_Parent_Call{Call: _e.mock.On("Parent", slot, parent)} | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | func (_c *mockhost_Parent_Call) Run(run func(slot Slot, parent Slot)) *mockhost_Parent_Call { | |
| 308 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 309 | + | var arg0 Slot | |
| 310 | + | if args[0] != nil { | |
| 311 | + | arg0 = args[0].(Slot) | |
| 312 | + | } | |
| 313 | + | var arg1 Slot | |
| 314 | + | if args[1] != nil { | |
| 315 | + | arg1 = args[1].(Slot) | |
| 316 | + | } | |
| 317 | + | run( | |
| 318 | + | arg0, | |
| 319 | + | arg1, | |
| 320 | + | ) | |
| 321 | + | }) | |
| 322 | + | return _c | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | func (_c *mockhost_Parent_Call) Return() *mockhost_Parent_Call { | |
| 326 | + | _c.Call.Return() | |
| 327 | + | return _c | |
| 328 | + | } | |
| 329 | + | ||
| 330 | + | func (_c *mockhost_Parent_Call) RunAndReturn(run func(slot Slot, parent Slot)) *mockhost_Parent_Call { | |
| 331 | + | _c.Run(run) | |
| 332 | + | return _c | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | // Ready provides a mock function for the type mockhost | |
| 336 | + | func (_mock *mockhost) Ready(slot Slot, info WindowInfo) { | |
| 337 | + | _mock.Called(slot, info) | |
| 338 | + | return | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | // mockhost_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' | |
| 342 | + | type mockhost_Ready_Call struct { | |
| 343 | + | *mock.Call | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | // Ready is a helper method to define mock.On call | |
| 347 | + | // - slot Slot | |
| 348 | + | // - info WindowInfo | |
| 349 | + | func (_e *mockhost_Expecter) Ready(slot any, info any) *mockhost_Ready_Call { | |
| 350 | + | return &mockhost_Ready_Call{Call: _e.mock.On("Ready", slot, info)} | |
| 351 | + | } | |
| 352 | + | ||
| 353 | + | func (_c *mockhost_Ready_Call) Run(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call { | |
| 354 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 355 | + | var arg0 Slot | |
| 356 | + | if args[0] != nil { | |
| 357 | + | arg0 = args[0].(Slot) | |
| 358 | + | } | |
| 359 | + | var arg1 WindowInfo | |
| 360 | + | if args[1] != nil { | |
| 361 | + | arg1 = args[1].(WindowInfo) | |
| 362 | + | } | |
| 363 | + | run( | |
| 364 | + | arg0, | |
| 365 | + | arg1, | |
| 366 | + | ) | |
| 367 | + | }) | |
| 368 | + | return _c | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | func (_c *mockhost_Ready_Call) Return() *mockhost_Ready_Call { | |
| 372 | + | _c.Call.Return() | |
| 373 | + | return _c | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | func (_c *mockhost_Ready_Call) RunAndReturn(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call { | |
| 377 | + | _c.Run(run) | |
| 378 | + | return _c | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | // Title provides a mock function for the type mockhost | |
| 382 | + | func (_mock *mockhost) Title(slot Slot, title string) { | |
| 383 | + | _mock.Called(slot, title) | |
| 384 | + | return | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | // mockhost_Title_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Title' | |
| 388 | + | type mockhost_Title_Call struct { | |
| 389 | + | *mock.Call | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | // Title is a helper method to define mock.On call | |
| 393 | + | // - slot Slot | |
| 394 | + | // - title string | |
| 395 | + | func (_e *mockhost_Expecter) Title(slot any, title any) *mockhost_Title_Call { | |
| 396 | + | return &mockhost_Title_Call{Call: _e.mock.On("Title", slot, title)} | |
| 397 | + | } | |
| 398 | + | ||
| 399 | + | func (_c *mockhost_Title_Call) Run(run func(slot Slot, title string)) *mockhost_Title_Call { | |
| 400 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 401 | + | var arg0 Slot | |
| 402 | + | if args[0] != nil { | |
| 403 | + | arg0 = args[0].(Slot) | |
| 404 | + | } | |
| 405 | + | var arg1 string | |
| 406 | + | if args[1] != nil { | |
| 407 | + | arg1 = args[1].(string) | |
| 408 | + | } | |
| 409 | + | run( | |
| 410 | + | arg0, | |
| 411 | + | arg1, | |
| 412 | + | ) | |
| 413 | + | }) | |
| 414 | + | return _c | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | func (_c *mockhost_Title_Call) Return() *mockhost_Title_Call { | |
| 418 | + | _c.Call.Return() | |
| 419 | + | return _c | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | func (_c *mockhost_Title_Call) RunAndReturn(run func(slot Slot, title string)) *mockhost_Title_Call { | |
| 423 | + | _c.Run(run) | |
| 424 | + | return _c | |
| 425 | + | } | |
A internal/adapters/wayland/xwm/mapflow.go +337 -0
| @@ -0,0 +1,337 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bnema.dev/xturbo" | |
| 5 | + | "bnema.dev/xturbo/xproto" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // ICCCM WM_STATE values. | |
| 9 | + | const ( | |
| 10 | + | wmStateWithdrawn uint32 = 0 | |
| 11 | + | wmStateNormal uint32 = 1 | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // _NET_WM_STATE client message actions. | |
| 15 | + | const ( | |
| 16 | + | netStateRemove = 0 | |
| 17 | + | netStateAdd = 1 | |
| 18 | + | netStateToggle = 2 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // beginMap starts the map flow of a managed window: every property, the | |
| 22 | + | // geometry and MapWindow go out in one batch (wlroots order). Also called by | |
| 23 | + | // Attach for windows mapped before it. | |
| 24 | + | func (x *XWM) beginMap(s Slot) { | |
| 25 | + | rec := x.tab.rec(s) | |
| 26 | + | if rec.kind != kindManaged || rec.mapping { | |
| 27 | + | return | |
| 28 | + | } | |
| 29 | + | rec.mapping, rec.ready = true, false | |
| 30 | + | rec.hasNetName = false | |
| 31 | + | rec.pending = propGeom | |
| 32 | + | xid := rec.xid | |
| 33 | + | for idx := 0; idx < propCount; idx++ { | |
| 34 | + | x.requestProp(s, idx) | |
| 35 | + | } | |
| 36 | + | b := x.conn.Buf() | |
| 37 | + | x.expect(xproto.GetGeometry(b, xproto.Drawable(xid)), kindGeom, x.tab.tag(s), 0) | |
| 38 | + | xproto.MapWindow(b, xproto.Window(xid)) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func (x *XWM) onMapRequest(ev *xturbo.Event) { | |
| 42 | + | if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 { | |
| 43 | + | x.beginMap(s) | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func (x *XWM) onMapNotify(ev *xturbo.Event) { | |
| 48 | + | if s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window())); s != 0 { | |
| 49 | + | x.tab.rec(s).mapped = true | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // onUnmapNotify ends the mapped life of a managed window; a later MapRequest | |
| 54 | + | // starts over and re-reads the properties. Synthetic events (ICCCM unmap | |
| 55 | + | // requests) are not an unmap. | |
| 56 | + | func (x *XWM) onUnmapNotify(ev *xturbo.Event) { | |
| 57 | + | if ev.SendEvent { | |
| 58 | + | return | |
| 59 | + | } | |
| 60 | + | s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window())) | |
| 61 | + | if s == 0 { | |
| 62 | + | return | |
| 63 | + | } | |
| 64 | + | rec := x.tab.rec(s) | |
| 65 | + | rec.mapped = false | |
| 66 | + | if rec.kind != kindManaged { | |
| 67 | + | return | |
| 68 | + | } | |
| 69 | + | wasMapping := rec.mapping | |
| 70 | + | rec.ready, rec.mapping, rec.pending, rec.sized = false, false, 0, false | |
| 71 | + | rec.gen++ // replies still in flight for this map name a dead tag | |
| 72 | + | if rec.gen == 0 { | |
| 73 | + | rec.gen = 1 | |
| 74 | + | } | |
| 75 | + | rec.pend.dirty = false | |
| 76 | + | rec.serial, rec.surface = 0, 0 | |
| 77 | + | if wasMapping { | |
| 78 | + | xid := rec.xid | |
| 79 | + | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateWithdrawn, 0}) | |
| 80 | + | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(xid), xproto.Atom(x.atoms[atomNetWMState])) | |
| 81 | + | } | |
| 82 | + | x.dropWindow(s) | |
| 83 | + | if rec.id != 0 { | |
| 84 | + | x.host.Dissociate(s, false) | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | // setProp32Typed is setProp32 with an explicit property type atom. | |
| 89 | + | func (x *XWM) setProp32Typed(win uint32, prop int, typ uint32, vals []uint32) { | |
| 90 | + | x.setProp32(win, prop, xproto.Atom(typ), vals) | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // checkReady fires Ready once every awaited reply is in. | |
| 94 | + | func (x *XWM) checkReady(s Slot) { | |
| 95 | + | rec := x.tab.rec(s) | |
| 96 | + | if rec == nil || rec.pending != 0 || rec.ready || !rec.mapping || rec.kind != kindManaged || x.host == nil { | |
| 97 | + | return | |
| 98 | + | } | |
| 99 | + | rec.ready = true | |
| 100 | + | x.classify(rec) | |
| 101 | + | info := x.info(s) | |
| 102 | + | xid := rec.xid | |
| 103 | + | ||
| 104 | + | var state [2]uint32 | |
| 105 | + | n := 0 | |
| 106 | + | if rec.modal { | |
| 107 | + | state[n] = x.atoms[atomNetWMStateModal] | |
| 108 | + | n++ | |
| 109 | + | } | |
| 110 | + | if rec.fullscreen { | |
| 111 | + | state[n] = x.atoms[atomNetWMStateFullscreen] | |
| 112 | + | n++ | |
| 113 | + | } | |
| 114 | + | x.setProp32(xid, atomNetWMState, xproto.AtomAtom, state[:n]) | |
| 115 | + | x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateNormal, 0}) | |
| 116 | + | x.addClient(xid) | |
| 117 | + | ||
| 118 | + | x.host.Ready(s, info) | |
| 119 | + | x.tryPair(s) | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | // info is what the host learns when a window is ready. | |
| 123 | + | func (x *XWM) info(s Slot) WindowInfo { | |
| 124 | + | rec := x.tab.rec(s) | |
| 125 | + | parent := x.managedSlot(rec.transient) | |
| 126 | + | info := WindowInfo{ | |
| 127 | + | PID: int(rec.pid), | |
| 128 | + | Floating: rec.floating, | |
| 129 | + | Parent: parent, | |
| 130 | + | Modal: rec.modal, | |
| 131 | + | Fullscreen: rec.fullscreen, | |
| 132 | + | Title: rec.title, | |
| 133 | + | AppID: rec.class, | |
| 134 | + | } | |
| 135 | + | if info.Floating || rec.sized { | |
| 136 | + | info.Width, info.Height = int(rec.w), int(rec.h) | |
| 137 | + | } | |
| 138 | + | return info | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) { | |
| 142 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok { | |
| 143 | + | rec.x, rec.y, rec.w, rec.h = int32(g.X()), int32(g.Y()), uint32(g.Width()), uint32(g.Height()) | |
| 144 | + | } | |
| 145 | + | rec.pending &^= propGeom | |
| 146 | + | x.checkReady(s) | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // onClientMessage handles WL_SURFACE_SERIAL (pairing), the legacy | |
| 150 | + | // WL_SURFACE_ID (counted, ignored) and _NET_WM_STATE requests. | |
| 151 | + | func (x *XWM) onClientMessage(ev *xturbo.Event) { | |
| 152 | + | e := xproto.ClientMessageOf(ev) | |
| 153 | + | if e.Format() != 32 { | |
| 154 | + | return | |
| 155 | + | } | |
| 156 | + | typ := uint32(e.Type()) | |
| 157 | + | switch typ { | |
| 158 | + | case x.atoms[atomWLSurfaceSerial]: | |
| 159 | + | s := x.tab.lookup(uint32(e.Window())) | |
| 160 | + | if s == 0 { | |
| 161 | + | return | |
| 162 | + | } | |
| 163 | + | rec := x.tab.rec(s) | |
| 164 | + | if rec.serial != 0 { | |
| 165 | + | x.log.Debug().Uint32("xid", rec.xid).Msg("duplicate WL_SURFACE_SERIAL ignored") | |
| 166 | + | return | |
| 167 | + | } | |
| 168 | + | d := e.Data().Data32() | |
| 169 | + | rec.serial = uint64(d[1])<<32 | uint64(d[0]) | |
| 170 | + | x.tryPair(s) | |
| 171 | + | case x.atoms[atomWLSurfaceID]: | |
| 172 | + | x.stats.LegacyID++ | |
| 173 | + | if x.stats.LegacyID == 1 { | |
| 174 | + | x.log.Warn().Msg("WL_SURFACE_ID received: Xwayland did not bind xwayland_shell_v1, window ignored") | |
| 175 | + | } | |
| 176 | + | case x.atoms[atomNetWMState]: | |
| 177 | + | x.onNetWMState(e) | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | // onNetWMState forwards fullscreen requests to the host and records them; the | |
| 182 | + | // property itself is written when core answers with a configure. | |
| 183 | + | func (x *XWM) onNetWMState(e xproto.ClientMessageEvent) { | |
| 184 | + | s := x.tab.lookup(uint32(e.Window())) | |
| 185 | + | if s == 0 { | |
| 186 | + | return | |
| 187 | + | } | |
| 188 | + | rec := x.tab.rec(s) | |
| 189 | + | if rec.kind != kindManaged { | |
| 190 | + | return | |
| 191 | + | } | |
| 192 | + | d := e.Data().Data32() | |
| 193 | + | apply := func(cur bool) bool { | |
| 194 | + | switch d[0] { | |
| 195 | + | case netStateRemove: | |
| 196 | + | return false | |
| 197 | + | case netStateAdd: | |
| 198 | + | return true | |
| 199 | + | case netStateToggle: | |
| 200 | + | return !cur | |
| 201 | + | } | |
| 202 | + | return cur | |
| 203 | + | } | |
| 204 | + | for _, a := range d[1:3] { | |
| 205 | + | switch { | |
| 206 | + | case a == 0: | |
| 207 | + | case a == x.atoms[atomNetWMStateModal]: | |
| 208 | + | rec.modal = apply(rec.modal) | |
| 209 | + | case a == x.atoms[atomNetWMStateFullscreen]: | |
| 210 | + | on := apply(rec.fullscreen) | |
| 211 | + | if on == rec.fullscreen { | |
| 212 | + | continue | |
| 213 | + | } | |
| 214 | + | rec.fullscreen = on | |
| 215 | + | if rec.ready { | |
| 216 | + | x.host.Fullscreen(s, on) | |
| 217 | + | rec = x.tab.rec(s) | |
| 218 | + | } | |
| 219 | + | } | |
| 220 | + | } | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | // onReparentNotify keeps the table to root's direct children: a window moved | |
| 224 | + | // away is forgotten, one moved into root is treated as newly created. | |
| 225 | + | func (x *XWM) onReparentNotify(ev *xturbo.Event) { | |
| 226 | + | e := xproto.ReparentNotifyOf(ev) | |
| 227 | + | xid := uint32(e.Window()) | |
| 228 | + | if xid == x.wmWin { | |
| 229 | + | return | |
| 230 | + | } | |
| 231 | + | s := x.tab.lookup(xid) | |
| 232 | + | if uint32(e.Parent()) != x.root { | |
| 233 | + | if s != 0 { | |
| 234 | + | x.forgetWindow(s) | |
| 235 | + | } | |
| 236 | + | return | |
| 237 | + | } | |
| 238 | + | if s != 0 { | |
| 239 | + | return | |
| 240 | + | } | |
| 241 | + | s = x.insert(xid) | |
| 242 | + | if s == 0 { | |
| 243 | + | return | |
| 244 | + | } | |
| 245 | + | rec := x.tab.rec(s) | |
| 246 | + | rec.override = e.OverrideRedirect() | |
| 247 | + | rec.x, rec.y = int32(e.X()), int32(e.Y()) | |
| 248 | + | if rec.override { | |
| 249 | + | rec.kind = kindUnmanaged | |
| 250 | + | x.stats.UnmanagedSeen++ | |
| 251 | + | return | |
| 252 | + | } | |
| 253 | + | if x.host != nil { | |
| 254 | + | x.adopt(s) | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | // tryPair links the window to the wl_surface carrying its serial, once both | |
| 259 | + | // are known and the window is ready (the host maps on the surface's first | |
| 260 | + | // buffer, so it must know the window's info by then). Unmanaged windows keep | |
| 261 | + | // the surface but are never associated. | |
| 262 | + | func (x *XWM) tryPair(s Slot) { | |
| 263 | + | rec := x.tab.rec(s) | |
| 264 | + | if rec.serial == 0 || rec.surface != 0 { | |
| 265 | + | return | |
| 266 | + | } | |
| 267 | + | if rec.kind == kindManaged && !rec.ready { | |
| 268 | + | return | |
| 269 | + | } | |
| 270 | + | for i := 0; i < x.unpairedN; i++ { | |
| 271 | + | if x.unpaired[i].serial != rec.serial { | |
| 272 | + | continue | |
| 273 | + | } | |
| 274 | + | rec.surface = x.unpaired[i].surface | |
| 275 | + | x.unpairedN-- | |
| 276 | + | x.unpaired[i] = x.unpaired[x.unpairedN] | |
| 277 | + | if rec.kind == kindManaged { | |
| 278 | + | x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired") | |
| 279 | + | x.host.Associate(s, rec.surface) | |
| 280 | + | } | |
| 281 | + | return | |
| 282 | + | } | |
| 283 | + | } | |
| 284 | + | ||
| 285 | + | // SetSerial records the serial of a wl_surface (xwayland_surface_v1.set_serial) | |
| 286 | + | // and pairs it with the window announcing the same serial. | |
| 287 | + | func (x *XWM) SetSerial(surfaceID uint64, serial uint64) { | |
| 288 | + | if serial == 0 || x.closed { | |
| 289 | + | return | |
| 290 | + | } | |
| 291 | + | for i := 0; i < x.unpairedN; i++ { | |
| 292 | + | if x.unpaired[i].serial == serial { | |
| 293 | + | x.unpaired[i].surface = surfaceID | |
| 294 | + | x.pairSerial(serial) | |
| 295 | + | return | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | if x.unpairedN == unpairedCap { | |
| 299 | + | copy(x.unpaired[:], x.unpaired[1:]) | |
| 300 | + | x.unpairedN-- | |
| 301 | + | if !x.warnedUnpaired { | |
| 302 | + | x.warnedUnpaired = true | |
| 303 | + | x.log.Warn().Msg("too many unpaired surfaces, dropping the oldest") | |
| 304 | + | } | |
| 305 | + | } | |
| 306 | + | x.unpaired[x.unpairedN] = struct{ serial, surface uint64 }{serial, surfaceID} | |
| 307 | + | x.unpairedN++ | |
| 308 | + | x.pairSerial(serial) | |
| 309 | + | } | |
| 310 | + | ||
| 311 | + | // pairSerial offers a newly known surface serial to the windows. | |
| 312 | + | func (x *XWM) pairSerial(serial uint64) { | |
| 313 | + | for i := range x.tab.recs { | |
| 314 | + | if r := &x.tab.recs[i]; r.kind != kindFree && r.serial == serial && r.surface == 0 { | |
| 315 | + | x.tryPair(Slot(i + 1)) | |
| 316 | + | break | |
| 317 | + | } | |
| 318 | + | } | |
| 319 | + | x.wake() | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | // SurfaceGone forgets a destroyed wl_surface. | |
| 323 | + | func (x *XWM) SurfaceGone(surfaceID uint64) { | |
| 324 | + | for i := 0; i < x.unpairedN; i++ { | |
| 325 | + | if x.unpaired[i].surface == surfaceID { | |
| 326 | + | x.unpairedN-- | |
| 327 | + | x.unpaired[i] = x.unpaired[x.unpairedN] | |
| 328 | + | break | |
| 329 | + | } | |
| 330 | + | } | |
| 331 | + | for i := range x.tab.recs { | |
| 332 | + | if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID { | |
| 333 | + | r.surface = 0 | |
| 334 | + | } | |
| 335 | + | } | |
| 336 | + | x.wake() | |
| 337 | + | } | |
A internal/adapters/wayland/xwm/mapflow_test.go +779 -0
| @@ -0,0 +1,779 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "bnema.dev/xturbo" | |
| 9 | + | "bnema.dev/xturbo/xproto" | |
| 10 | + | "github.com/bnema/neferwl/internal/ports" | |
| 11 | + | "github.com/stretchr/testify/mock" | |
| 12 | + | "github.com/stretchr/testify/require" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // harness bundles an attached XWM, its peer and the host mock. | |
| 16 | + | type harness struct { | |
| 17 | + | t *testing.T | |
| 18 | + | x *XWM | |
| 19 | + | p *xpeer | |
| 20 | + | h *mockhost | |
| 21 | + | ||
| 22 | + | ready map[uint32]WindowInfo // by xid, filled by Ready | |
| 23 | + | next ports.WindowID | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func newHarness(t *testing.T, mutate func(p *xpeer)) *harness { | |
| 27 | + | t.Helper() | |
| 28 | + | x, p := bootPeer(t, mutate) | |
| 29 | + | hs := &harness{t: t, x: x, p: p, h: newMockhost(t), ready: map[uint32]WindowInfo{}} | |
| 30 | + | src := newMockfdSource(t) | |
| 31 | + | src.EXPECT().Remove().Maybe() | |
| 32 | + | src.EXPECT().SetWritable(mock.Anything).Return(nil).Maybe() | |
| 33 | + | post, _ := newPost() | |
| 34 | + | x.attach(src, hs.h, post) | |
| 35 | + | return hs | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func (hs *harness) expectCreated(win uint32) { | |
| 39 | + | hs.next++ | |
| 40 | + | hs.h.EXPECT().Created(mock.Anything, win).Return(hs.next).Once() | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (hs *harness) expectReady(win uint32) { | |
| 44 | + | hs.h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, info WindowInfo) { | |
| 45 | + | hs.ready[win] = info | |
| 46 | + | }).Once() | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func (hs *harness) create(win uint32) Slot { | |
| 50 | + | hs.t.Helper() | |
| 51 | + | hs.expectCreated(win) | |
| 52 | + | hs.p.createNotify(win, false) | |
| 53 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 }) | |
| 54 | + | return hs.x.tab.lookup(win) | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // mapAndWait creates and maps win and waits for Ready. | |
| 58 | + | func (hs *harness) mapAndWait(win uint32) Slot { | |
| 59 | + | hs.t.Helper() | |
| 60 | + | s := hs.create(win) | |
| 61 | + | hs.expectReady(win) | |
| 62 | + | hs.p.mapRequest(win) | |
| 63 | + | readUntil(hs.t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 64 | + | return s | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func (hs *harness) str(win uint32, atom string, typ uint32, s string) { | |
| 68 | + | hs.p.setProp(win, hs.p.atom(atom), typ, 8, []byte(s)) | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func (hs *harness) words(win uint32, atom string, typ uint32, w ...uint32) { | |
| 72 | + | hs.p.setProp(win, hs.p.atom(atom), typ, 32, le32(w...)) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (hs *harness) atoms(win uint32, atom string, names ...string) { | |
| 76 | + | var w []uint32 | |
| 77 | + | for _, n := range names { | |
| 78 | + | w = append(w, hs.p.atom(n)) | |
| 79 | + | } | |
| 80 | + | hs.words(win, atom, uint32(xproto.AtomAtom), w...) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func (hs *harness) classic(win uint32) { | |
| 84 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("inst\x00Class\x00")) | |
| 85 | + | hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "net title") | |
| 86 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("old title")) | |
| 87 | + | hs.words(win, "_NET_WM_PID", uint32(xproto.AtomCardinal), 4242) | |
| 88 | + | hs.p.set(func(p *xpeer) { p.geoms[win] = xpeerGeom{w: 300, h: 200} }) | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | func (hs *harness) count(match func(r xpeerReq) bool) int { | |
| 92 | + | n := 0 | |
| 93 | + | for _, r := range hs.p.requests() { | |
| 94 | + | if match(r) { | |
| 95 | + | n++ | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | return n | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | func (hs *harness) waitMap(win uint32) { | |
| 102 | + | hs.t.Helper() | |
| 103 | + | readUntil(hs.t, hs.x, func() bool { | |
| 104 | + | return hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win }) > 0 | |
| 105 | + | }) | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | func TestMapFlowBatchesRequestsAndReadyFiresOnce(t *testing.T) { | |
| 109 | + | hs := newHarness(t, nil) | |
| 110 | + | const win = 0x500001 | |
| 111 | + | hs.classic(win) | |
| 112 | + | hs.create(win) | |
| 113 | + | base := len(hs.p.requests()) | |
| 114 | + | ||
| 115 | + | hs.p.holdReplies() | |
| 116 | + | hs.p.mapRequest(win) | |
| 117 | + | hs.expectReady(win) | |
| 118 | + | hs.waitMap(win) | |
| 119 | + | ||
| 120 | + | reqs := hs.p.requests()[base:] | |
| 121 | + | want := []uint32{ | |
| 122 | + | uint32(xproto.AtomWmClass), uint32(xproto.AtomWmName), hs.p.atom("_NET_WM_NAME"), uint32(xproto.AtomWmHints), | |
| 123 | + | uint32(xproto.AtomWmNormalHints), hs.p.atom("WM_PROTOCOLS"), uint32(xproto.AtomWmTransientFor), | |
| 124 | + | hs.p.atom("_NET_WM_WINDOW_TYPE"), hs.p.atom("_NET_WM_STATE"), hs.p.atom("_NET_WM_PID"), | |
| 125 | + | } | |
| 126 | + | require.GreaterOrEqual(t, len(reqs), len(want)+2) | |
| 127 | + | for i, a := range want { | |
| 128 | + | require.EqualValues(t, opGetProperty, reqs[i].major, i) | |
| 129 | + | require.Equal(t, uint32(win), reqs[i].u32(4)) | |
| 130 | + | require.Equal(t, a, reqs[i].u32(8), i) | |
| 131 | + | require.EqualValues(t, 1024, reqs[i].u32(20), "long length: one 4 KiB read") | |
| 132 | + | } | |
| 133 | + | require.EqualValues(t, opGetGeometry, reqs[len(want)].major) | |
| 134 | + | require.EqualValues(t, opMapWindow, reqs[len(want)+1].major, "MapWindow in the same batch") | |
| 135 | + | ||
| 136 | + | hs.p.release() // every reply arrives after MapWindow went out | |
| 137 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 138 | + | info := hs.ready[win] | |
| 139 | + | require.Equal(t, WindowInfo{PID: 4242, Title: "net title", AppID: "Class"}, info, "_NET_WM_NAME wins, class is the second string") | |
| 140 | + | ||
| 141 | + | state, ok := hs.p.prop(win, "WM_STATE") | |
| 142 | + | require.True(t, ok) | |
| 143 | + | require.Equal(t, []uint32{1, 0}, atomsOf(state)) | |
| 144 | + | list, ok := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 145 | + | require.True(t, ok) | |
| 146 | + | require.Equal(t, []uint32{win}, atomsOf(list)) | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func TestMapFlowFloatingAndModal(t *testing.T) { | |
| 150 | + | hint := func(minw, minh, maxw, maxh uint32) []byte { | |
| 151 | + | return le32(hintMinSize|hintMaxSize, 0, 0, 0, 0, minw, minh, maxw, maxh, 0, 0, 0, 0, 0, 0, 0, 0, 0) | |
| 152 | + | } | |
| 153 | + | cases := []struct { | |
| 154 | + | name string | |
| 155 | + | setup func(hs *harness, win uint32) | |
| 156 | + | want func(info WindowInfo) | |
| 157 | + | }{ | |
| 158 | + | {"dialog type", func(hs *harness, w uint32) { | |
| 159 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_DIALOG") | |
| 160 | + | }, func(i WindowInfo) { | |
| 161 | + | require.True(t, i.Floating) | |
| 162 | + | require.Equal(t, 300, i.Width) | |
| 163 | + | require.Equal(t, 200, i.Height) | |
| 164 | + | }}, | |
| 165 | + | {"utility type", func(hs *harness, w uint32) { | |
| 166 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_UTILITY") | |
| 167 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |
| 168 | + | {"splash type", func(hs *harness, w uint32) { | |
| 169 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_SPLASH") | |
| 170 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |
| 171 | + | {"normal type", func(hs *harness, w uint32) { | |
| 172 | + | hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_NORMAL") | |
| 173 | + | }, func(i WindowInfo) { require.False(t, i.Floating); require.Zero(t, i.Width) }}, | |
| 174 | + | {"fixed size", func(hs *harness, w uint32) { | |
| 175 | + | hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(200, 100, 200, 100)) | |
| 176 | + | }, func(i WindowInfo) { require.True(t, i.Floating) }}, | |
| 177 | + | {"resizable", func(hs *harness, w uint32) { | |
| 178 | + | hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(100, 100, 800, 600)) | |
| 179 | + | }, func(i WindowInfo) { require.False(t, i.Floating) }}, | |
| 180 | + | {"modal", func(hs *harness, w uint32) { | |
| 181 | + | hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_MODAL") | |
| 182 | + | }, func(i WindowInfo) { require.True(t, i.Modal); require.False(t, i.Fullscreen) }}, | |
| 183 | + | {"fullscreen at map", func(hs *harness, w uint32) { | |
| 184 | + | hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_FULLSCREEN") | |
| 185 | + | }, func(i WindowInfo) { require.True(t, i.Fullscreen) }}, | |
| 186 | + | } | |
| 187 | + | for _, c := range cases { | |
| 188 | + | t.Run(c.name, func(t *testing.T) { | |
| 189 | + | hs := newHarness(t, nil) | |
| 190 | + | const win = 0x500001 | |
| 191 | + | hs.classic(win) | |
| 192 | + | c.setup(hs, win) | |
| 193 | + | hs.mapAndWait(win) | |
| 194 | + | c.want(hs.ready[win]) | |
| 195 | + | }) | |
| 196 | + | } | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | func TestMapFlowTransientForManagedWindowFloats(t *testing.T) { | |
| 200 | + | hs := newHarness(t, nil) | |
| 201 | + | const parent, child = 0x500001, 0x500002 | |
| 202 | + | hs.classic(parent) | |
| 203 | + | ps := hs.mapAndWait(parent) | |
| 204 | + | hs.classic(child) | |
| 205 | + | hs.p.setProp(child, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(parent)) | |
| 206 | + | hs.mapAndWait(child) | |
| 207 | + | require.True(t, hs.ready[child].Floating) | |
| 208 | + | require.Equal(t, ps, hs.ready[child].Parent) | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | func TestMapFlowTransientForUnknownWindowIsNotAParent(t *testing.T) { | |
| 212 | + | hs := newHarness(t, nil) | |
| 213 | + | const win = 0x500001 | |
| 214 | + | hs.classic(win) | |
| 215 | + | hs.p.setProp(win, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(0x777777)) | |
| 216 | + | hs.mapAndWait(win) | |
| 217 | + | require.Zero(t, hs.ready[win].Parent) | |
| 218 | + | require.False(t, hs.ready[win].Floating) | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | func TestMapFlowClassFallsBackToInstance(t *testing.T) { | |
| 222 | + | hs := newHarness(t, nil) | |
| 223 | + | const win = 0x500001 | |
| 224 | + | hs.classic(win) | |
| 225 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("onlyinstance\x00")) | |
| 226 | + | hs.mapAndWait(win) | |
| 227 | + | require.Equal(t, "onlyinstance", hs.ready[win].AppID) | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | func TestMapFlowTitleFallsBackToWMName(t *testing.T) { | |
| 231 | + | hs := newHarness(t, nil) | |
| 232 | + | const win = 0x500001 | |
| 233 | + | hs.classic(win) | |
| 234 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |
| 235 | + | hs.mapAndWait(win) | |
| 236 | + | require.Equal(t, "old title", hs.ready[win].Title) | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | func TestMapFlowPIDFromXRes(t *testing.T) { | |
| 240 | + | hs := newHarness(t, func(p *xpeer) { p.pids = map[uint32]uint32{0x500001: 777} }) | |
| 241 | + | const win = 0x500001 | |
| 242 | + | hs.classic(win) | |
| 243 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |
| 244 | + | hs.create(win) | |
| 245 | + | hs.expectReady(win) | |
| 246 | + | hs.p.mapRequest(win) | |
| 247 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 248 | + | require.Equal(t, 777, hs.ready[win].PID, "Ready waited for the QueryClientIds reply") | |
| 249 | + | require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == 144 && r.minor == 4 })) | |
| 250 | + | require.Zero(t, hs.x.tab.rec(hs.x.tab.lookup(win)).pending) | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | func TestMapFlowPIDUnknownStillReady(t *testing.T) { | |
| 254 | + | hs := newHarness(t, nil) | |
| 255 | + | const win = 0x500001 | |
| 256 | + | hs.classic(win) | |
| 257 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |
| 258 | + | hs.mapAndWait(win) | |
| 259 | + | require.Zero(t, hs.ready[win].PID) | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | func TestMapFlowNoXResAsksNothing(t *testing.T) { | |
| 263 | + | exts := fullPeerExts() | |
| 264 | + | delete(exts, "X-Resource") | |
| 265 | + | fd, p := startXPeer(t, exts) | |
| 266 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 267 | + | require.NoError(t, err) | |
| 268 | + | t.Cleanup(x.CloseConn) | |
| 269 | + | h := newMockhost(t) | |
| 270 | + | src := newMockfdSource(t) | |
| 271 | + | src.EXPECT().Remove().Maybe() | |
| 272 | + | post, _ := newPost() | |
| 273 | + | x.attach(src, h, post) | |
| 274 | + | hs := &harness{t: t, x: x, p: p, h: h, ready: map[uint32]WindowInfo{}} | |
| 275 | + | const win = 0x500001 | |
| 276 | + | hs.classic(win) | |
| 277 | + | p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) }) | |
| 278 | + | hs.mapAndWait(win) | |
| 279 | + | require.Zero(t, hs.count(func(r xpeerReq) bool { return r.major == 144 })) | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | func TestMapFlowBadWindowOnPropertiesNeverReady(t *testing.T) { | |
| 283 | + | hs := newHarness(t, nil) | |
| 284 | + | const win = 0x500001 | |
| 285 | + | hs.classic(win) | |
| 286 | + | hs.p.set(func(p *xpeer) { p.bad[win] = true }) | |
| 287 | + | s := hs.create(win) | |
| 288 | + | hs.p.mapRequest(win) | |
| 289 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 }) | |
| 290 | + | require.Zero(t, hs.x.tab.rec(s).pending, "failed replies are no longer awaited") | |
| 291 | + | require.False(t, hs.x.tab.rec(s).ready) | |
| 292 | + | require.Zero(t, hs.x.Stats().Unattributed) | |
| 293 | + | ||
| 294 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 295 | + | hs.p.destroyNotify(win) | |
| 296 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | func TestMapRequestTwiceBeforeReadyAsksOnce(t *testing.T) { | |
| 300 | + | hs := newHarness(t, nil) | |
| 301 | + | const win = 0x500001 | |
| 302 | + | hs.classic(win) | |
| 303 | + | hs.create(win) | |
| 304 | + | hs.p.holdReplies() | |
| 305 | + | hs.p.mapRequest(win) | |
| 306 | + | hs.p.mapRequest(win) | |
| 307 | + | hs.expectReady(win) | |
| 308 | + | hs.waitMap(win) | |
| 309 | + | hs.p.release() | |
| 310 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 311 | + | require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win })) | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | func TestMapNotifyMarksMapped(t *testing.T) { | |
| 315 | + | hs := newHarness(t, nil) | |
| 316 | + | const win = 0x500001 | |
| 317 | + | hs.classic(win) | |
| 318 | + | s := hs.mapAndWait(win) | |
| 319 | + | hs.p.mapNotify(win) | |
| 320 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapped }) | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | // Property changes after Ready. | |
| 324 | + | ||
| 325 | + | func (hs *harness) notify(win uint32, atom string, deleted bool) { | |
| 326 | + | hs.t.Helper() | |
| 327 | + | before := hs.x.Stats().Replies | |
| 328 | + | hs.p.propertyNotify(win, hs.p.atom(atom), deleted) | |
| 329 | + | if deleted { | |
| 330 | + | hs.p.propertyNotify(win, hs.p.atom("WM_PROTOCOLS"), false) // sentinel: one reply follows | |
| 331 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 332 | + | return | |
| 333 | + | } | |
| 334 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | func TestPropertyChangedTitleReachesHostOnce(t *testing.T) { | |
| 338 | + | hs := newHarness(t, nil) | |
| 339 | + | const win = 0x500001 | |
| 340 | + | hs.classic(win) | |
| 341 | + | s := hs.mapAndWait(win) | |
| 342 | + | ||
| 343 | + | hs.h.EXPECT().Title(s, "renamed").Once() | |
| 344 | + | hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "renamed") | |
| 345 | + | hs.notify(win, "_NET_WM_NAME", false) | |
| 346 | + | require.Equal(t, "renamed", hs.x.tab.rec(s).title) | |
| 347 | + | ||
| 348 | + | // Same value again: no host call (the mock fails on an unexpected one). | |
| 349 | + | hs.notify(win, "_NET_WM_NAME", false) | |
| 350 | + | ||
| 351 | + | // WM_NAME is ignored while _NET_WM_NAME is set. | |
| 352 | + | hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("legacy")) | |
| 353 | + | before := hs.x.Stats().Events | |
| 354 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false) | |
| 355 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > before }) | |
| 356 | + | require.Equal(t, "renamed", hs.x.tab.rec(s).title) | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | func TestPropertyNetNameDeletedFallsBackToWMName(t *testing.T) { | |
| 360 | + | hs := newHarness(t, nil) | |
| 361 | + | const win = 0x500001 | |
| 362 | + | hs.classic(win) | |
| 363 | + | s := hs.mapAndWait(win) | |
| 364 | + | hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) }) | |
| 365 | + | hs.h.EXPECT().Title(s, "old title").Once() | |
| 366 | + | hs.p.propertyNotify(win, hs.p.atom("_NET_WM_NAME"), true) | |
| 367 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).title == "old title" }) | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | func TestPropertyClassChangeReachesHost(t *testing.T) { | |
| 371 | + | hs := newHarness(t, nil) | |
| 372 | + | const win = 0x500001 | |
| 373 | + | hs.classic(win) | |
| 374 | + | s := hs.mapAndWait(win) | |
| 375 | + | hs.h.EXPECT().AppID(s, "Other").Once() | |
| 376 | + | hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00Other\x00")) | |
| 377 | + | hs.p.propertyNotify(win, uint32(xproto.AtomWmClass), false) | |
| 378 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).class == "Other" }) | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | func TestPropertyTransientChangeReachesHost(t *testing.T) { | |
| 382 | + | hs := newHarness(t, nil) | |
| 383 | + | const a, b = 0x500001, 0x500002 | |
| 384 | + | hs.classic(a) | |
| 385 | + | as := hs.mapAndWait(a) | |
| 386 | + | hs.classic(b) | |
| 387 | + | bs := hs.mapAndWait(b) | |
| 388 | + | hs.h.EXPECT().Parent(bs, as).Once() | |
| 389 | + | hs.p.setProp(b, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(a)) | |
| 390 | + | hs.p.propertyNotify(b, uint32(xproto.AtomWmTransientFor), false) | |
| 391 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(bs).transient == a }) | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | func TestPropertyInputProtocolsAndHints(t *testing.T) { | |
| 395 | + | hs := newHarness(t, nil) | |
| 396 | + | const win = 0x500001 | |
| 397 | + | hs.classic(win) | |
| 398 | + | hs.atoms(win, "WM_PROTOCOLS", "WM_DELETE_WINDOW", "WM_TAKE_FOCUS") | |
| 399 | + | hs.p.setProp(win, uint32(xproto.AtomWmHints), uint32(xproto.AtomWmHints), 32, le32(hintInput, 0, 0, 0, 0, 0, 0, 0, 0)) | |
| 400 | + | s := hs.mapAndWait(win) | |
| 401 | + | r := hs.x.tab.rec(s) | |
| 402 | + | require.True(t, r.deleteWindow) | |
| 403 | + | require.True(t, r.takeFocus) | |
| 404 | + | require.False(t, r.input, "WM_HINTS input = false") | |
| 405 | + | ||
| 406 | + | // No WM_HINTS: input defaults to true. | |
| 407 | + | const other = 0x500002 | |
| 408 | + | hs.classic(other) | |
| 409 | + | o := hs.mapAndWait(other) | |
| 410 | + | require.True(t, hs.x.tab.rec(o).input) | |
| 411 | + | require.False(t, hs.x.tab.rec(o).deleteWindow) | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | func TestPropertyUnwatchedAtomAsksNothing(t *testing.T) { | |
| 415 | + | hs := newHarness(t, nil) | |
| 416 | + | const win = 0x500001 | |
| 417 | + | hs.classic(win) | |
| 418 | + | hs.mapAndWait(win) | |
| 419 | + | n := len(hs.p.requests()) | |
| 420 | + | hs.p.propertyNotify(win, hs.p.atom("_MOTIF_WM_HINTS"), false) | |
| 421 | + | hs.p.propertyNotify(0x999999, hs.p.atom("_NET_WM_NAME"), false) // unknown window | |
| 422 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |
| 423 | + | require.Len(t, hs.p.requests(), n) | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | // Pairing. | |
| 427 | + | ||
| 428 | + | func (hs *harness) serialMsg(win uint32, serial uint64) { | |
| 429 | + | hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_SERIAL"), uint32(serial), uint32(serial>>32)) | |
| 430 | + | hs.t.Helper() | |
| 431 | + | before := hs.x.Stats().Events | |
| 432 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events > before }) | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | func TestPairingSerialBeforeSetSerial(t *testing.T) { | |
| 436 | + | hs := newHarness(t, nil) | |
| 437 | + | const win = 0x500001 | |
| 438 | + | hs.classic(win) | |
| 439 | + | s := hs.mapAndWait(win) | |
| 440 | + | hs.serialMsg(win, 0x1_0000_0002) | |
| 441 | + | require.EqualValues(t, 0x1_0000_0002, hs.x.tab.rec(s).serial, "high word in data32[1]") | |
| 442 | + | hs.h.EXPECT().Associate(s, uint64(99)).Once() | |
| 443 | + | hs.x.SetSerial(99, 0x1_0000_0002) | |
| 444 | + | require.EqualValues(t, 99, hs.x.tab.rec(s).surface) | |
| 445 | + | require.Zero(t, hs.x.unpairedN) | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | func TestPairingSetSerialBeforeSerial(t *testing.T) { | |
| 449 | + | hs := newHarness(t, nil) | |
| 450 | + | const win = 0x500001 | |
| 451 | + | hs.classic(win) | |
| 452 | + | s := hs.mapAndWait(win) | |
| 453 | + | hs.x.SetSerial(99, 7) | |
| 454 | + | require.Equal(t, 1, hs.x.unpairedN) | |
| 455 | + | hs.h.EXPECT().Associate(s, uint64(99)).Once() | |
| 456 | + | hs.serialMsg(win, 7) | |
| 457 | + | require.Zero(t, hs.x.unpairedN) | |
| 458 | + | } | |
| 459 | + | ||
| 460 | + | func TestPairingWaitsForReady(t *testing.T) { | |
| 461 | + | hs := newHarness(t, nil) | |
| 462 | + | const win = 0x500001 | |
| 463 | + | hs.classic(win) | |
| 464 | + | s := hs.create(win) | |
| 465 | + | hs.x.SetSerial(5, 3) | |
| 466 | + | hs.serialMsg(win, 3) // not mapped yet: no Associate (the mock fails on one) | |
| 467 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |
| 468 | + | ||
| 469 | + | hs.h.EXPECT().Associate(s, uint64(5)).Once() | |
| 470 | + | hs.expectReady(win) | |
| 471 | + | hs.p.mapRequest(win) | |
| 472 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).surface != 0 }) | |
| 473 | + | } | |
| 474 | + | ||
| 475 | + | func TestPairingSerialForUnknownWindowIgnored(t *testing.T) { | |
| 476 | + | hs := newHarness(t, nil) | |
| 477 | + | hs.x.SetSerial(5, 3) | |
| 478 | + | hs.serialMsg(0x888888, 3) | |
| 479 | + | require.Equal(t, 1, hs.x.unpairedN, "stays unpaired") | |
| 480 | + | } | |
| 481 | + | ||
| 482 | + | func TestPairingSurfaceGoneBeforePair(t *testing.T) { | |
| 483 | + | hs := newHarness(t, nil) | |
| 484 | + | const win = 0x500001 | |
| 485 | + | hs.classic(win) | |
| 486 | + | s := hs.mapAndWait(win) | |
| 487 | + | hs.x.SetSerial(5, 3) | |
| 488 | + | hs.x.SurfaceGone(5) | |
| 489 | + | require.Zero(t, hs.x.unpairedN) | |
| 490 | + | hs.serialMsg(win, 3) // the surface is gone: nothing to pair with | |
| 491 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | func TestPairingSurfaceGoneAfterPairClearsLink(t *testing.T) { | |
| 495 | + | hs := newHarness(t, nil) | |
| 496 | + | const win = 0x500001 | |
| 497 | + | hs.classic(win) | |
| 498 | + | s := hs.mapAndWait(win) | |
| 499 | + | hs.serialMsg(win, 3) | |
| 500 | + | hs.h.EXPECT().Associate(s, uint64(5)).Once() | |
| 501 | + | hs.x.SetSerial(5, 3) | |
| 502 | + | hs.x.SurfaceGone(5) | |
| 503 | + | require.Zero(t, hs.x.tab.rec(s).surface) | |
| 504 | + | } | |
| 505 | + | ||
| 506 | + | func TestPairingDuplicateSerialIgnored(t *testing.T) { | |
| 507 | + | hs := newHarness(t, nil) | |
| 508 | + | const win = 0x500001 | |
| 509 | + | hs.classic(win) | |
| 510 | + | s := hs.mapAndWait(win) | |
| 511 | + | hs.serialMsg(win, 3) | |
| 512 | + | hs.serialMsg(win, 4) | |
| 513 | + | require.EqualValues(t, 3, hs.x.tab.rec(s).serial) | |
| 514 | + | hs.x.SetSerial(8, 4) // nobody announces 4: stays unpaired | |
| 515 | + | require.Equal(t, 1, hs.x.unpairedN) | |
| 516 | + | } | |
| 517 | + | ||
| 518 | + | func TestPairingUnmanagedKeepsSurfaceWithoutAssociate(t *testing.T) { | |
| 519 | + | hs := newHarness(t, nil) | |
| 520 | + | hs.p.createNotify(0x500009, true) | |
| 521 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |
| 522 | + | hs.serialMsg(0x500009, 3) | |
| 523 | + | hs.x.SetSerial(5, 3) // the mock fails on an Associate | |
| 524 | + | require.EqualValues(t, 5, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).surface) | |
| 525 | + | } | |
| 526 | + | ||
| 527 | + | func TestPairingTableOverflowDropsOldest(t *testing.T) { | |
| 528 | + | hs := newHarness(t, nil) | |
| 529 | + | for i := uint64(1); i <= unpairedCap+1; i++ { | |
| 530 | + | hs.x.SetSerial(100+i, i) | |
| 531 | + | } | |
| 532 | + | require.Equal(t, unpairedCap, hs.x.unpairedN) | |
| 533 | + | for i := 0; i < hs.x.unpairedN; i++ { | |
| 534 | + | require.NotEqual(t, uint64(1), hs.x.unpaired[i].serial, "oldest dropped") | |
| 535 | + | } | |
| 536 | + | } | |
| 537 | + | ||
| 538 | + | func TestPairingZeroSerialIgnored(t *testing.T) { | |
| 539 | + | hs := newHarness(t, nil) | |
| 540 | + | hs.x.SetSerial(5, 0) | |
| 541 | + | require.Zero(t, hs.x.unpairedN) | |
| 542 | + | } | |
| 543 | + | ||
| 544 | + | func TestLegacySurfaceIDCountedAndIgnored(t *testing.T) { | |
| 545 | + | hs := newHarness(t, nil) | |
| 546 | + | const win = 0x500001 | |
| 547 | + | hs.classic(win) | |
| 548 | + | s := hs.mapAndWait(win) | |
| 549 | + | hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_ID"), 12) | |
| 550 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().LegacyID == 1 }) | |
| 551 | + | require.Zero(t, hs.x.tab.rec(s).serial) | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | func TestNetWMStateClientMessageForwardsFullscreen(t *testing.T) { | |
| 555 | + | hs := newHarness(t, nil) | |
| 556 | + | const win = 0x500001 | |
| 557 | + | hs.classic(win) | |
| 558 | + | s := hs.mapAndWait(win) | |
| 559 | + | hs.h.EXPECT().Fullscreen(s, true).Once() | |
| 560 | + | hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 1, hs.p.atom("_NET_WM_STATE_FULLSCREEN")) | |
| 561 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).fullscreen }) | |
| 562 | + | hs.h.EXPECT().Fullscreen(s, false).Once() // toggle | |
| 563 | + | hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 2, hs.p.atom("_NET_WM_STATE_FULLSCREEN")) | |
| 564 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).fullscreen }) | |
| 565 | + | // Not written back by the message itself. | |
| 566 | + | st, ok := hs.p.prop(win, "_NET_WM_STATE") | |
| 567 | + | require.True(t, ok) | |
| 568 | + | require.Empty(t, st.data) | |
| 569 | + | } | |
| 570 | + | ||
| 571 | + | // Unmap, destroy, remap. | |
| 572 | + | ||
| 573 | + | func TestUnmapDissociatesAndRemapRereadsProperties(t *testing.T) { | |
| 574 | + | hs := newHarness(t, nil) | |
| 575 | + | const win = 0x500001 | |
| 576 | + | hs.classic(win) | |
| 577 | + | s := hs.mapAndWait(win) | |
| 578 | + | ||
| 579 | + | // A synthetic UnmapNotify (ICCCM withdraw request) is not an unmap. | |
| 580 | + | hs.p.unmapNotify(win, true) | |
| 581 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |
| 582 | + | require.True(t, hs.x.tab.rec(s).ready) | |
| 583 | + | ||
| 584 | + | hs.h.EXPECT().Dissociate(s, false).Once() | |
| 585 | + | hs.p.unmapNotify(win, false) | |
| 586 | + | readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).ready }) | |
| 587 | + | r := hs.x.tab.rec(s) | |
| 588 | + | require.False(t, r.mapping) | |
| 589 | + | require.Zero(t, r.serial) | |
| 590 | + | hs.p.waitReq(0, func(q xpeerReq) bool { | |
| 591 | + | return q.major == opSetInputFocus || q.major == opChangeProperty && q.u32(4) == win | |
| 592 | + | }) | |
| 593 | + | readUntil(t, hs.x, func() bool { | |
| 594 | + | v, ok := hs.p.prop(win, "WM_STATE") | |
| 595 | + | return ok && len(v.data) == 8 && binary.LittleEndian.Uint32(v.data) == 0 | |
| 596 | + | }) | |
| 597 | + | _, ok := hs.p.prop(win, "_NET_WM_STATE") | |
| 598 | + | require.False(t, ok, "_NET_WM_STATE deleted") | |
| 599 | + | list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 600 | + | require.Empty(t, list.data) | |
| 601 | + | ||
| 602 | + | // Remap starts over. | |
| 603 | + | delete(hs.ready, win) | |
| 604 | + | hs.expectReady(win) | |
| 605 | + | n := hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win }) | |
| 606 | + | hs.p.mapRequest(win) | |
| 607 | + | readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok }) | |
| 608 | + | require.Equal(t, n+propCount, hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win })) | |
| 609 | + | list, _ = hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 610 | + | require.Equal(t, []uint32{win}, atomsOf(list)) | |
| 611 | + | } | |
| 612 | + | ||
| 613 | + | func TestDestroyWhileMappingNeverReady(t *testing.T) { | |
| 614 | + | hs := newHarness(t, nil) | |
| 615 | + | const win = 0x500001 | |
| 616 | + | hs.classic(win) | |
| 617 | + | s := hs.create(win) | |
| 618 | + | hs.p.holdReplies() | |
| 619 | + | hs.p.mapRequest(win) | |
| 620 | + | hs.waitMap(win) | |
| 621 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 622 | + | hs.p.destroyNotify(win) | |
| 623 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |
| 624 | + | replies := hs.x.Stats().Replies | |
| 625 | + | hs.p.release() // replies for a window that is gone: xturbo retired their slots | |
| 626 | + | readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) }) | |
| 627 | + | require.Equal(t, replies, hs.x.Stats().Replies, "none reaches the XWM") | |
| 628 | + | } | |
| 629 | + | ||
| 630 | + | func TestStaleRepliesAfterSlotReuseAreIgnored(t *testing.T) { | |
| 631 | + | hs := newHarness(t, nil) | |
| 632 | + | const old, fresh = 0x500001, 0x500002 | |
| 633 | + | hs.classic(old) | |
| 634 | + | hs.classic(fresh) | |
| 635 | + | s := hs.create(old) | |
| 636 | + | hs.p.holdReplies() | |
| 637 | + | hs.p.mapRequest(old) | |
| 638 | + | hs.waitMap(old) | |
| 639 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 640 | + | hs.p.destroyNotify(old) | |
| 641 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(old) == 0 }) | |
| 642 | + | ||
| 643 | + | hs.expectCreated(fresh) | |
| 644 | + | hs.p.createNotify(fresh, false) | |
| 645 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(fresh) != 0 }) | |
| 646 | + | require.Equal(t, s, hs.x.tab.lookup(fresh), "the freed slot is reused") | |
| 647 | + | ||
| 648 | + | hs.p.release() | |
| 649 | + | readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) }) | |
| 650 | + | ||
| 651 | + | // A reply that does get through with the old tag (slot reused, generation | |
| 652 | + | // bumped) is ignored. | |
| 653 | + | oldTag := s | Slot(hs.x.tab.rec(s).gen-1)<<16 | |
| 654 | + | rep := make([]byte, 32) | |
| 655 | + | rep[0] = 1 | |
| 656 | + | hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{uint32(oldTag), propClass}, Raw: rep}) | |
| 657 | + | hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{uint32(oldTag)}, Raw: rep}) | |
| 658 | + | require.EqualValues(t, 2, hs.x.Stats().Stale) | |
| 659 | + | require.False(t, hs.x.tab.rec(s).ready, "stale replies must not complete the new window") | |
| 660 | + | require.Zero(t, hs.x.tab.rec(s).pending) | |
| 661 | + | } | |
| 662 | + | ||
| 663 | + | // Reparenting. | |
| 664 | + | ||
| 665 | + | func TestReparentAwayFromRootRemovesManagedWindow(t *testing.T) { | |
| 666 | + | hs := newHarness(t, nil) | |
| 667 | + | const win = 0x500001 | |
| 668 | + | hs.classic(win) | |
| 669 | + | s := hs.mapAndWait(win) | |
| 670 | + | hs.h.EXPECT().Dissociate(s, true).Once() | |
| 671 | + | hs.p.reparentNotify(win, 0x600000) | |
| 672 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 }) | |
| 673 | + | list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST") | |
| 674 | + | require.Empty(t, list.data) | |
| 675 | + | require.Zero(t, hs.x.tab.n) | |
| 676 | + | } | |
| 677 | + | ||
| 678 | + | func TestReparentAwayOfUnmanagedWindowRemovesIt(t *testing.T) { | |
| 679 | + | hs := newHarness(t, nil) | |
| 680 | + | hs.p.createNotify(0x500009, true) | |
| 681 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |
| 682 | + | hs.p.reparentNotify(0x500009, 0x600000) | |
| 683 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) == 0 }) | |
| 684 | + | } | |
| 685 | + | ||
| 686 | + | func TestReparentIntoRootAdoptsWindow(t *testing.T) { | |
| 687 | + | hs := newHarness(t, nil) | |
| 688 | + | const win = 0x500001 | |
| 689 | + | hs.expectCreated(win) | |
| 690 | + | hs.p.reparentNotify(win, peerRoot) | |
| 691 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 }) | |
| 692 | + | require.Equal(t, kindManaged, hs.x.tab.rec(hs.x.tab.lookup(win)).kind) | |
| 693 | + | ||
| 694 | + | // Already known: no second Created. | |
| 695 | + | hs.p.reparentNotify(win, peerRoot) | |
| 696 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 }) | |
| 697 | + | } | |
| 698 | + | ||
| 699 | + | func TestReparentIntoRootOfOverrideRedirectWindowStaysUnmanaged(t *testing.T) { | |
| 700 | + | hs := newHarness(t, nil) | |
| 701 | + | hs.p.event(21, func(b *[32]byte) { | |
| 702 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 703 | + | binary.LittleEndian.PutUint32(b[8:], 0x500009) | |
| 704 | + | binary.LittleEndian.PutUint32(b[12:], peerRoot) | |
| 705 | + | b[20] = 1 | |
| 706 | + | }) | |
| 707 | + | readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 }) | |
| 708 | + | require.Equal(t, kindUnmanaged, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).kind) | |
| 709 | + | } | |
| 710 | + | ||
| 711 | + | func TestReparentOfWMWindowIgnored(t *testing.T) { | |
| 712 | + | hs := newHarness(t, nil) | |
| 713 | + | hs.p.reparentNotify(hs.x.wmWin, peerRoot) | |
| 714 | + | readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 1 }) | |
| 715 | + | require.Zero(t, hs.x.tab.lookup(hs.x.wmWin)) | |
| 716 | + | } | |
| 717 | + | ||
| 718 | + | // Window known only before Attach: Attach replays the map flow. | |
| 719 | + | func TestAttachMapsWindowsRequestedBeforeAttach(t *testing.T) { | |
| 720 | + | x, p := bootPeer(t, nil) | |
| 721 | + | const win = 0x500001 | |
| 722 | + | p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00")) | |
| 723 | + | p.createNotify(win, false) | |
| 724 | + | p.mapRequest(win) | |
| 725 | + | readUntil(t, x, func() bool { return x.Stats().Events >= 2 }) | |
| 726 | + | h := newMockhost(t) | |
| 727 | + | src := newMockfdSource(t) | |
| 728 | + | src.EXPECT().Remove().Maybe() | |
| 729 | + | post, _ := newPost() | |
| 730 | + | var got WindowInfo | |
| 731 | + | done := false | |
| 732 | + | h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(1)).Once() | |
| 733 | + | h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once() | |
| 734 | + | x.attach(src, h, post) | |
| 735 | + | readUntil(t, x, func() bool { return done }) | |
| 736 | + | require.Equal(t, "B", got.AppID) | |
| 737 | + | } | |
| 738 | + | ||
| 739 | + | // Replies complete a map in any order (a server may answer the geometry | |
| 740 | + | // before the properties it was asked for first): Ready fires on the last one. | |
| 741 | + | func TestMapFlowReadyFiresOnLastReplyInAnyOrder(t *testing.T) { | |
| 742 | + | hs := newHarness(t, nil) | |
| 743 | + | const win = 0x500001 | |
| 744 | + | hs.classic(win) | |
| 745 | + | s := hs.create(win) | |
| 746 | + | r := hs.x.tab.rec(s) | |
| 747 | + | r.mapping, r.pending = true, propMaskAll&^propClientIDs | |
| 748 | + | hs.expectReady(win) | |
| 749 | + | order := []int{propCount, propPID, propClass, propNetName, propName, propHints, propNormalHints, propProtocols, propTransientFor, propWindowType, propState} | |
| 750 | + | for i, bit := range order { | |
| 751 | + | require.False(t, hs.x.tab.rec(s).ready, i) | |
| 752 | + | if bit == propCount { // the geometry | |
| 753 | + | hs.x.tab.rec(s).w, hs.x.tab.rec(s).h = 300, 200 | |
| 754 | + | hs.x.tab.rec(s).pending &^= propGeom | |
| 755 | + | hs.x.checkReady(s) | |
| 756 | + | continue | |
| 757 | + | } | |
| 758 | + | var val []byte | |
| 759 | + | var format uint8 = 8 | |
| 760 | + | switch bit { | |
| 761 | + | case propClass: | |
| 762 | + | val = []byte("a\x00B\x00") | |
| 763 | + | case propNetName: | |
| 764 | + | val = []byte("t") | |
| 765 | + | case propPID: | |
| 766 | + | val, format = le32(5), 32 | |
| 767 | + | default: | |
| 768 | + | format = 32 | |
| 769 | + | } | |
| 770 | + | x := hs.x | |
| 771 | + | x.applyProp(s, bit, format, val) | |
| 772 | + | x.tab.rec(s).pending &^= 1 << bit | |
| 773 | + | x.checkReady(s) | |
| 774 | + | } | |
| 775 | + | require.True(t, hs.x.tab.rec(s).ready) | |
| 776 | + | require.Equal(t, "B", hs.ready[win].AppID) | |
| 777 | + | require.Equal(t, "t", hs.ready[win].Title) | |
| 778 | + | require.Equal(t, 5, hs.ready[win].PID) | |
| 779 | + | } | |
A internal/adapters/wayland/xwm/props.go +356 -0
| @@ -0,0 +1,356 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "strings" | |
| 7 | + | ||
| 8 | + | "bnema.dev/xturbo" | |
| 9 | + | "bnema.dev/xturbo/res" | |
| 10 | + | "bnema.dev/xturbo/xproto" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Property indices (second Expect argument of kindProp) and their bits in | |
| 14 | + | // record.pending. propGeom is the GetGeometry bit; propClientIDs is raised | |
| 15 | + | // while a QueryClientIds reply stands in for a missing _NET_WM_PID. | |
| 16 | + | const ( | |
| 17 | + | propClass = iota | |
| 18 | + | propName | |
| 19 | + | propNetName | |
| 20 | + | propHints | |
| 21 | + | propNormalHints | |
| 22 | + | propProtocols | |
| 23 | + | propTransientFor | |
| 24 | + | propWindowType | |
| 25 | + | propState | |
| 26 | + | propPID | |
| 27 | + | propCount | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | const ( | |
| 31 | + | propGeom uint16 = 1 << propCount | |
| 32 | + | propClientIDs uint16 = propGeom << 1 | |
| 33 | + | // propMaskAll is what a map waits for: every property and the geometry. | |
| 34 | + | propMaskAll uint16 = propGeom<<1 - 1 | |
| 35 | + | ) | |
| 36 | + | ||
| 37 | + | // WM_NORMAL_HINTS flags and WM_HINTS flags. | |
| 38 | + | const ( | |
| 39 | + | hintInput = 1 << 0 | |
| 40 | + | hintMinSize = 1 << 4 | |
| 41 | + | hintMaxSize = 1 << 5 | |
| 42 | + | ) | |
| 43 | + | ||
| 44 | + | // maxPropWords bounds one GetProperty (4 KiB), so titles are truncated there. | |
| 45 | + | const maxPropWords = 1024 | |
| 46 | + | ||
| 47 | + | // propIndex maps a property atom to its index, or -1 when it is not watched. | |
| 48 | + | func (x *XWM) propIndex(a uint32) int { | |
| 49 | + | switch a { | |
| 50 | + | case uint32(xproto.AtomWmClass): | |
| 51 | + | return propClass | |
| 52 | + | case uint32(xproto.AtomWmName): | |
| 53 | + | return propName | |
| 54 | + | case uint32(xproto.AtomWmHints): | |
| 55 | + | return propHints | |
| 56 | + | case uint32(xproto.AtomWmNormalHints): | |
| 57 | + | return propNormalHints | |
| 58 | + | case uint32(xproto.AtomWmTransientFor): | |
| 59 | + | return propTransientFor | |
| 60 | + | case x.atoms[atomNetWMName]: | |
| 61 | + | return propNetName | |
| 62 | + | case x.atoms[atomWMProtocols]: | |
| 63 | + | return propProtocols | |
| 64 | + | case x.atoms[atomNetWMWindowType]: | |
| 65 | + | return propWindowType | |
| 66 | + | case x.atoms[atomNetWMState]: | |
| 67 | + | return propState | |
| 68 | + | case x.atoms[atomNetWMPID]: | |
| 69 | + | return propPID | |
| 70 | + | } | |
| 71 | + | return -1 | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func (x *XWM) propAtom(idx int) xproto.Atom { | |
| 75 | + | switch idx { | |
| 76 | + | case propClass: | |
| 77 | + | return xproto.AtomWmClass | |
| 78 | + | case propName: | |
| 79 | + | return xproto.AtomWmName | |
| 80 | + | case propHints: | |
| 81 | + | return xproto.AtomWmHints | |
| 82 | + | case propNormalHints: | |
| 83 | + | return xproto.AtomWmNormalHints | |
| 84 | + | case propTransientFor: | |
| 85 | + | return xproto.AtomWmTransientFor | |
| 86 | + | case propNetName: | |
| 87 | + | return xproto.Atom(x.atoms[atomNetWMName]) | |
| 88 | + | case propProtocols: | |
| 89 | + | return xproto.Atom(x.atoms[atomWMProtocols]) | |
| 90 | + | case propWindowType: | |
| 91 | + | return xproto.Atom(x.atoms[atomNetWMWindowType]) | |
| 92 | + | case propState: | |
| 93 | + | return xproto.Atom(x.atoms[atomNetWMState]) | |
| 94 | + | default: | |
| 95 | + | return xproto.Atom(x.atoms[atomNetWMPID]) | |
| 96 | + | } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // requestProp queues a GetProperty for property idx of s. | |
| 100 | + | func (x *XWM) requestProp(s Slot, idx int) { | |
| 101 | + | rec := x.tab.rec(s) | |
| 102 | + | seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid), x.propAtom(idx), xproto.AtomAny, 0, maxPropWords) | |
| 103 | + | rec.pending |= 1 << idx | |
| 104 | + | x.expect(seq, kindProp, x.tab.tag(s), uint32(idx)) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // onPropertyNotify handles a changed property of a managed window that is | |
| 108 | + | // being mapped or is mapped; unwatched atoms cost nothing. | |
| 109 | + | func (x *XWM) onPropertyNotify(ev *xturbo.Event) { | |
| 110 | + | e := xproto.PropertyNotifyOf(ev) | |
| 111 | + | s := x.tab.lookup(uint32(e.Window())) | |
| 112 | + | if s == 0 { | |
| 113 | + | return | |
| 114 | + | } | |
| 115 | + | if rec := x.tab.rec(s); rec.kind != kindManaged || !rec.mapping { | |
| 116 | + | return | |
| 117 | + | } else if rec.ready && x.propIndex(uint32(e.Atom())) == propState { | |
| 118 | + | // The XWM owns _NET_WM_STATE of a mapped window: our own writes | |
| 119 | + | // would feed back, and clients change it with ClientMessages. | |
| 120 | + | return | |
| 121 | + | } | |
| 122 | + | idx := x.propIndex(uint32(e.Atom())) | |
| 123 | + | if idx < 0 { | |
| 124 | + | return | |
| 125 | + | } | |
| 126 | + | if e.State() == xproto.PropertyDelete { | |
| 127 | + | x.applyProp(s, idx, 0, nil) | |
| 128 | + | return | |
| 129 | + | } | |
| 130 | + | x.requestProp(s, idx) | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // onPropReply decodes one GetProperty reply into the record. An absent | |
| 134 | + | // property (format 0) clears its fields. | |
| 135 | + | func (x *XWM) onPropReply(s Slot, _ *record, r *xturbo.Reply) { | |
| 136 | + | idx := int(r.Args[1]) | |
| 137 | + | if idx < 0 || idx >= propCount { | |
| 138 | + | return | |
| 139 | + | } | |
| 140 | + | var format uint8 | |
| 141 | + | var val []byte | |
| 142 | + | if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok { | |
| 143 | + | format, val = rep.Format(), rep.Value() | |
| 144 | + | } | |
| 145 | + | x.applyProp(s, idx, format, val) | |
| 146 | + | x.tab.rec(s).pending &^= 1 << idx | |
| 147 | + | x.checkReady(s) | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | func word(b []byte, i int) uint32 { return binary.LittleEndian.Uint32(b[4*i:]) } | |
| 151 | + | ||
| 152 | + | // applyProp stores one decoded property. It does not allocate unless a string | |
| 153 | + | // changed. Host calls come last: the record is not touched after them. | |
| 154 | + | func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) { | |
| 155 | + | rec := x.tab.rec(s) | |
| 156 | + | if idx == propClass || idx == propName || idx == propNetName { | |
| 157 | + | if format != 8 { | |
| 158 | + | val = nil | |
| 159 | + | } | |
| 160 | + | } else if format != 32 { | |
| 161 | + | val = nil | |
| 162 | + | } | |
| 163 | + | switch idx { | |
| 164 | + | case propClass: | |
| 165 | + | inst, class := val, []byte(nil) | |
| 166 | + | if i := bytes.IndexByte(val, 0); i >= 0 { | |
| 167 | + | inst, class = val[:i], val[i+1:] | |
| 168 | + | if j := bytes.IndexByte(class, 0); j >= 0 { | |
| 169 | + | class = class[:j] | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | app := class | |
| 173 | + | if len(app) == 0 { | |
| 174 | + | app = inst | |
| 175 | + | } | |
| 176 | + | if rec.class == string(app) { | |
| 177 | + | return | |
| 178 | + | } | |
| 179 | + | cls := strings.ToValidUTF8(string(app), "\uFFFD") | |
| 180 | + | if rec.class == cls { | |
| 181 | + | return | |
| 182 | + | } | |
| 183 | + | rec.class = cls | |
| 184 | + | if rec.ready { | |
| 185 | + | x.host.AppID(s, rec.class) | |
| 186 | + | } | |
| 187 | + | case propName: | |
| 188 | + | if !rec.hasNetName { | |
| 189 | + | x.setTitle(s, val) | |
| 190 | + | } | |
| 191 | + | case propNetName: | |
| 192 | + | had := rec.hasNetName | |
| 193 | + | rec.hasNetName = len(val) > 0 | |
| 194 | + | if rec.hasNetName { | |
| 195 | + | x.setTitle(s, val) | |
| 196 | + | } else if had && rec.mapping { | |
| 197 | + | x.requestProp(s, propName) | |
| 198 | + | } | |
| 199 | + | case propHints: | |
| 200 | + | rec.input = true | |
| 201 | + | if len(val) >= 8 && word(val, 0)&hintInput != 0 { | |
| 202 | + | rec.input = word(val, 1) != 0 | |
| 203 | + | } | |
| 204 | + | case propNormalHints: | |
| 205 | + | rec.minW, rec.minH, rec.maxW, rec.maxH = 0, 0, 0, 0 | |
| 206 | + | if len(val) >= 36 { | |
| 207 | + | fl := word(val, 0) | |
| 208 | + | if fl&hintMinSize != 0 { | |
| 209 | + | rec.minW, rec.minH = int32(word(val, 5)), int32(word(val, 6)) | |
| 210 | + | } | |
| 211 | + | if fl&hintMaxSize != 0 { | |
| 212 | + | rec.maxW, rec.maxH = int32(word(val, 7)), int32(word(val, 8)) | |
| 213 | + | } | |
| 214 | + | } | |
| 215 | + | x.classify(rec) | |
| 216 | + | case propProtocols: | |
| 217 | + | rec.deleteWindow, rec.takeFocus = false, false | |
| 218 | + | for i := 0; i+4 <= len(val); i += 4 { | |
| 219 | + | switch binary.LittleEndian.Uint32(val[i:]) { | |
| 220 | + | case x.atoms[atomWMDeleteWindow]: | |
| 221 | + | rec.deleteWindow = true | |
| 222 | + | case x.atoms[atomWMTakeFocus]: | |
| 223 | + | rec.takeFocus = true | |
| 224 | + | } | |
| 225 | + | } | |
| 226 | + | case propTransientFor: | |
| 227 | + | var t uint32 | |
| 228 | + | if len(val) >= 4 { | |
| 229 | + | t = word(val, 0) | |
| 230 | + | } | |
| 231 | + | if t == rec.transient { | |
| 232 | + | return | |
| 233 | + | } | |
| 234 | + | rec.transient = t | |
| 235 | + | x.classify(rec) | |
| 236 | + | if rec.ready { | |
| 237 | + | x.host.Parent(s, x.managedSlot(t)) | |
| 238 | + | } | |
| 239 | + | case propWindowType: | |
| 240 | + | rec.wtype = 0 | |
| 241 | + | for i := 0; i+4 <= len(val); i += 4 { | |
| 242 | + | if t := x.windowType(binary.LittleEndian.Uint32(val[i:])); t != 0 { | |
| 243 | + | rec.wtype = t | |
| 244 | + | break | |
| 245 | + | } | |
| 246 | + | } | |
| 247 | + | x.classify(rec) | |
| 248 | + | case propState: | |
| 249 | + | var modal, fs bool | |
| 250 | + | for i := 0; i+4 <= len(val); i += 4 { | |
| 251 | + | switch binary.LittleEndian.Uint32(val[i:]) { | |
| 252 | + | case x.atoms[atomNetWMStateModal]: | |
| 253 | + | modal = true | |
| 254 | + | case x.atoms[atomNetWMStateFullscreen]: | |
| 255 | + | fs = true | |
| 256 | + | } | |
| 257 | + | } | |
| 258 | + | changed := fs != rec.fullscreen | |
| 259 | + | rec.modal, rec.fullscreen = modal, fs | |
| 260 | + | if changed && rec.ready { | |
| 261 | + | x.host.Fullscreen(s, fs) | |
| 262 | + | } | |
| 263 | + | case propPID: | |
| 264 | + | if len(val) >= 4 { | |
| 265 | + | rec.pid = int32(word(val, 0)) | |
| 266 | + | return | |
| 267 | + | } | |
| 268 | + | rec.pid = 0 | |
| 269 | + | if !rec.ready && rec.pending&propClientIDs == 0 && x.conn.Ext().Info(xturbo.ExtRes).Present { | |
| 270 | + | x.queryClientIDs(s) | |
| 271 | + | } | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | // setTitle stores a title when it differs; only a change allocates. | |
| 276 | + | func (x *XWM) setTitle(s Slot, val []byte) { | |
| 277 | + | rec := x.tab.rec(s) | |
| 278 | + | if rec.title == string(val) { | |
| 279 | + | return | |
| 280 | + | } | |
| 281 | + | t := strings.ToValidUTF8(string(val), "\uFFFD") | |
| 282 | + | if rec.title == t { | |
| 283 | + | return | |
| 284 | + | } | |
| 285 | + | rec.title = t | |
| 286 | + | if rec.ready { | |
| 287 | + | x.host.Title(s, rec.title) | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | // windowType returns the atom enum of a recognised _NET_WM_WINDOW_TYPE atom, | |
| 292 | + | // or 0. | |
| 293 | + | func (x *XWM) windowType(a uint32) uint8 { | |
| 294 | + | for t := atomNetWMWindowTypeNormal; t <= atomNetWMWindowTypeDND; t++ { | |
| 295 | + | if x.atoms[t] == a { | |
| 296 | + | return uint8(t) | |
| 297 | + | } | |
| 298 | + | } | |
| 299 | + | return 0 | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | // floatingType reports whether a window type floats by itself. | |
| 303 | + | func floatingType(t uint8) bool { | |
| 304 | + | switch int(t) { | |
| 305 | + | case atomNetWMWindowTypeDialog, atomNetWMWindowTypeUtility, atomNetWMWindowTypeSplash, atomNetWMWindowTypeToolbar: | |
| 306 | + | return true | |
| 307 | + | } | |
| 308 | + | return false | |
| 309 | + | } | |
| 310 | + | ||
| 311 | + | // classify sets rec.floating, the single source of truth: a dialog-like | |
| 312 | + | // window type, a transient for a managed window, or a fixed size. | |
| 313 | + | func (x *XWM) classify(rec *record) { | |
| 314 | + | fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH | |
| 315 | + | rec.floating = floatingType(rec.wtype) || x.managedSlot(rec.transient) != 0 || fixed | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | // managedSlot returns the slot of the managed window xid, or 0. | |
| 319 | + | func (x *XWM) managedSlot(xid uint32) Slot { | |
| 320 | + | if xid == 0 { | |
| 321 | + | return 0 | |
| 322 | + | } | |
| 323 | + | if s := x.tab.lookup(xid); s != 0 && x.tab.rec(s).kind == kindManaged { | |
| 324 | + | return s | |
| 325 | + | } | |
| 326 | + | return 0 | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | // queryClientIDs asks XRes for the PID of the client owning s, which stands | |
| 330 | + | // in for a missing _NET_WM_PID: Ready waits for it (propClientIDs). | |
| 331 | + | func (x *XWM) queryClientIDs(s Slot) { | |
| 332 | + | rec := x.tab.rec(s) | |
| 333 | + | specs := [1]res.ClientIdSpec{{Client: rec.xid, Mask: res.ClientIdMaskLocalClientPID}} | |
| 334 | + | seq := res.QueryClientIds(x.conn.Buf(), specs[:]) | |
| 335 | + | if seq == 0 { | |
| 336 | + | return | |
| 337 | + | } | |
| 338 | + | rec.pending |= propClientIDs | |
| 339 | + | x.expect(seq, kindClientIds, x.tab.tag(s), 0) | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | // onClientIdsReply stores the PID XRes found and releases the map. | |
| 343 | + | func (x *XWM) onClientIdsReply(s Slot, rec *record, r *xturbo.Reply) { | |
| 344 | + | if q, ok := res.ParseQueryClientIdsReply(r.Raw); ok { | |
| 345 | + | it := q.Ids() | |
| 346 | + | for it.Next() { | |
| 347 | + | v := it.Value() | |
| 348 | + | if v.Spec().Mask == res.ClientIdMaskLocalClientPID && v.Value().Len() > 0 { | |
| 349 | + | rec.pid = int32(v.Value().At(0)) | |
| 350 | + | break | |
| 351 | + | } | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | rec.pending &^= propClientIDs | |
| 355 | + | x.checkReady(s) | |
| 356 | + | } | |
A internal/adapters/wayland/xwm/streams_test.go +183 -0
| @@ -0,0 +1,183 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/hex" | |
| 5 | + | "os" | |
| 6 | + | "path/filepath" | |
| 7 | + | "runtime" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "bnema.dev/xturbo/xproto" | |
| 13 | + | "github.com/stretchr/testify/require" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // The recorded streams are the bytes an X server wrote to the XWM during a | |
| 17 | + | // steady-state session of one mapped window, captured from the scripted xpeer | |
| 18 | + | // (real encoded X packets: events, GetProperty replies). The scenario is | |
| 19 | + | // deterministic, so a replay meets the request sequence numbers it recorded. | |
| 20 | + | // They are not xtrace captures of a real Xwayland: xpeer speaks the same wire | |
| 21 | + | // protocol and keeps the fixture reproducible. Regenerate with | |
| 22 | + | // | |
| 23 | + | // XWM_UPDATE_STREAMS=1 go test ./internal/adapters/wayland/xwm -run TestRecordedStreams | |
| 24 | + | const ( | |
| 25 | + | streamCycles = 400 | |
| 26 | + | streamWarm = 20 // cycles replayed before measuring | |
| 27 | + | ) | |
| 28 | + | ||
| 29 | + | const streamWin = 0x500001 | |
| 30 | + | ||
| 31 | + | type streamKind struct { | |
| 32 | + | name string | |
| 33 | + | // perCycle is Events+Replies the XWM sees per cycle. | |
| 34 | + | perCycle int | |
| 35 | + | cycle func(hs *harness) | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | var streamKinds = []streamKind{ | |
| 39 | + | // A title notification the XWM answers with one GetProperty whose reply | |
| 40 | + | // equals the stored title. | |
| 41 | + | {"property_unchanged", 2, func(hs *harness) { | |
| 42 | + | before := hs.x.Stats().Replies | |
| 43 | + | hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false) | |
| 44 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 45 | + | }}, | |
| 46 | + | // The same plus ConfigureNotify, FocusIn and MapNotify. | |
| 47 | + | {"steady", 5, func(hs *harness) { | |
| 48 | + | before := hs.x.Stats().Replies | |
| 49 | + | hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false) | |
| 50 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before }) | |
| 51 | + | events := hs.x.Stats().Events | |
| 52 | + | hs.p.configureNotify(streamWin) | |
| 53 | + | hs.p.focusIn(streamWin) | |
| 54 | + | hs.p.mapNotify(streamWin) | |
| 55 | + | readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events >= events+3 }) | |
| 56 | + | }}, | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // streamScenario boots, maps one window and parks the peer behind a poke so | |
| 60 | + | // nothing but the test writes to the XWM afterwards (an allocation count is | |
| 61 | + | // process-wide). The poke is part of the scenario, recorded or replayed. | |
| 62 | + | func streamScenario(t *testing.T) *harness { | |
| 63 | + | t.Helper() | |
| 64 | + | hs := newHarness(t, nil) | |
| 65 | + | hs.classic(streamWin) | |
| 66 | + | hs.mapAndWait(streamWin) | |
| 67 | + | // The steady state is a focused window: a FocusIn on it is not denied, | |
| 68 | + | // so no request depends on how events are batched into flushes. The | |
| 69 | + | // poke makes sure the focus requests are out before the peer pauses. | |
| 70 | + | hs.x.Focus(hs.x.tab.lookup(streamWin)) | |
| 71 | + | poke(hs) | |
| 72 | + | return hs | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func poke(hs *harness) { | |
| 76 | + | hs.t.Helper() | |
| 77 | + | n := len(hs.p.requests()) | |
| 78 | + | xproto.GetInputFocus(hs.x.conn.Buf()) | |
| 79 | + | hs.x.flush() | |
| 80 | + | hs.p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus }) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | func recordStream(t *testing.T, k streamKind) []byte { | |
| 84 | + | hs := streamScenario(t) | |
| 85 | + | poke(hs) | |
| 86 | + | hs.p.mu.Lock() | |
| 87 | + | hs.p.record = true | |
| 88 | + | hs.p.mu.Unlock() | |
| 89 | + | for i := 0; i < streamCycles; i++ { | |
| 90 | + | k.cycle(hs) | |
| 91 | + | } | |
| 92 | + | hs.p.mu.Lock() | |
| 93 | + | defer hs.p.mu.Unlock() | |
| 94 | + | hs.p.record = false | |
| 95 | + | return append([]byte(nil), hs.p.tap...) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func streamPath(k streamKind) string { | |
| 99 | + | return filepath.Join("testdata", "streams", k.name+".hex") | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func loadStream(t *testing.T, k streamKind) []byte { | |
| 103 | + | t.Helper() | |
| 104 | + | text, err := os.ReadFile(streamPath(k)) | |
| 105 | + | require.NoError(t, err) | |
| 106 | + | b, err := hex.DecodeString(strings.Join(strings.Fields(string(text)), "")) | |
| 107 | + | require.NoError(t, err) | |
| 108 | + | return b | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func encodeHex(b []byte) string { | |
| 112 | + | var sb strings.Builder | |
| 113 | + | for len(b) > 0 { | |
| 114 | + | n := min(32, len(b)) | |
| 115 | + | sb.WriteString(hex.EncodeToString(b[:n])) | |
| 116 | + | sb.WriteByte('\n') | |
| 117 | + | b = b[n:] | |
| 118 | + | } | |
| 119 | + | return sb.String() | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | func TestRecordedStreams(t *testing.T) { | |
| 123 | + | for _, k := range streamKinds { | |
| 124 | + | t.Run(k.name, func(t *testing.T) { | |
| 125 | + | got := recordStream(t, k) | |
| 126 | + | if os.Getenv("XWM_UPDATE_STREAMS") != "" { | |
| 127 | + | require.NoError(t, os.MkdirAll(filepath.Dir(streamPath(k)), 0o755)) | |
| 128 | + | require.NoError(t, os.WriteFile(streamPath(k), []byte(encodeHex(got)), 0o644)) | |
| 129 | + | } | |
| 130 | + | require.Equal(t, loadStream(t, k), got, "the committed stream no longer matches the protocol the XWM speaks") | |
| 131 | + | }) | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | // replay feeds the recorded stream and reports the mallocs of the part after | |
| 136 | + | // the warm-up cycles. | |
| 137 | + | func replay(t *testing.T, k streamKind) (mallocs uint64, hs *harness) { | |
| 138 | + | t.Helper() | |
| 139 | + | stream := loadStream(t, k) | |
| 140 | + | hs = streamScenario(t) | |
| 141 | + | hs.p.pause() | |
| 142 | + | poke(hs) | |
| 143 | + | _, err := hs.p.f.Write(stream) | |
| 144 | + | require.NoError(t, err) | |
| 145 | + | ||
| 146 | + | x := hs.x | |
| 147 | + | base := x.Stats().Events + x.Stats().Replies | |
| 148 | + | drive := func(until uint64) { | |
| 149 | + | deadline := time.Now().Add(10 * time.Second) | |
| 150 | + | for x.Stats().Events+x.Stats().Replies < until { | |
| 151 | + | if time.Now().After(deadline) { // no formatting arguments: boxing them would count as mallocs | |
| 152 | + | t.Fatal("stream stalled") | |
| 153 | + | } | |
| 154 | + | before := x.Stats().Events + x.Stats().Replies | |
| 155 | + | x.onReadable() | |
| 156 | + | if x.Stats().Events+x.Stats().Replies == before { | |
| 157 | + | runtime.Gosched() | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | drive(base + uint64(streamWarm*k.perCycle)) | |
| 162 | + | target := base + uint64(streamCycles*k.perCycle) | |
| 163 | + | ||
| 164 | + | var m0, m1 runtime.MemStats | |
| 165 | + | runtime.GC() | |
| 166 | + | runtime.ReadMemStats(&m0) | |
| 167 | + | drive(target) | |
| 168 | + | runtime.ReadMemStats(&m1) | |
| 169 | + | require.Equal(t, target, x.Stats().Events+x.Stats().Replies) | |
| 170 | + | return m1.Mallocs - m0.Mallocs, hs | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func TestPropertyUnchangedAllocations(t *testing.T) { | |
| 174 | + | m, hs := replay(t, streamKinds[0]) | |
| 175 | + | require.Zero(t, m, "unchanged property cycles must not allocate") | |
| 176 | + | require.Equal(t, "net title", hs.x.tab.rec(hs.x.tab.lookup(streamWin)).title) | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | func TestDispatchAllocations(t *testing.T) { | |
| 180 | + | m, hs := replay(t, streamKinds[1]) | |
| 181 | + | require.Zero(t, m, "steady-state event dispatch must not allocate") | |
| 182 | + | require.True(t, hs.x.tab.rec(hs.x.tab.lookup(streamWin)).mapped) | |
| 183 | + | } | |
A internal/adapters/wayland/xwm/table.go +228 -0
| @@ -0,0 +1,228 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "math/bits" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/neferwl/internal/ports" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // Record kinds. | |
| 10 | + | const ( | |
| 11 | + | kindFree uint8 = iota | |
| 12 | + | kindManaged | |
| 13 | + | kindUnmanaged | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | const ( | |
| 17 | + | initialSlots = 256 | |
| 18 | + | // maxSlots bounds the table: a Slot travels in the low 16 bits of an | |
| 19 | + | // Expect argument. | |
| 20 | + | maxSlots = 1<<16 - 1 | |
| 21 | + | indexGone = 0xFFFFFFFF // tombstone in table.index | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | // pending is the configure state not yet (or last) sent to the X server. | |
| 25 | + | type pending struct { | |
| 26 | + | x, y int32 | |
| 27 | + | w, h uint32 | |
| 28 | + | fullscreen, activated, dirty bool | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // record is one X window known to the XWM. Strings are the only per-record | |
| 32 | + | // heap data. | |
| 33 | + | type record struct { | |
| 34 | + | xid uint32 | |
| 35 | + | id ports.WindowID | |
| 36 | + | kind uint8 | |
| 37 | + | gen uint16 // bumped on insert; stale Expect replies are told apart by it | |
| 38 | + | ||
| 39 | + | // X state | |
| 40 | + | override, mapped, mapping, ready bool | |
| 41 | + | x, y int32 | |
| 42 | + | w, h uint32 | |
| 43 | + | ||
| 44 | + | // properties | |
| 45 | + | pending uint16 // propMask bits still awaited | |
| 46 | + | input, deleteWindow, takeFocus bool | |
| 47 | + | wtype uint8 | |
| 48 | + | modal, fullscreen bool | |
| 49 | + | transient uint32 // xid | |
| 50 | + | pid int32 | |
| 51 | + | minW, minH, maxW, maxH int32 | |
| 52 | + | title, class string // owned, replaced only on change | |
| 53 | + | hasNetName bool | |
| 54 | + | floating bool // dialog-like type; set with the properties | |
| 55 | + | sized bool // the client asked for its size before Ready | |
| 56 | + | ||
| 57 | + | // pairing | |
| 58 | + | serial uint64 | |
| 59 | + | surface uint64 | |
| 60 | + | ||
| 61 | + | // configure | |
| 62 | + | pend, sent pending | |
| 63 | + | stateSent uint8 // _NET_WM_STATE bits last written | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // table is a slab of records with an open-addressing index by X id. No Go map. | |
| 67 | + | type table struct { | |
| 68 | + | recs []record // slab, len == cap, power of two | |
| 69 | + | free []uint32 // free slot indices (LIFO) | |
| 70 | + | index []uint32 // slot+1, 0 empty, indexGone tombstone; len == 2*len(recs) | |
| 71 | + | shift uint8 | |
| 72 | + | n int | |
| 73 | + | tombs int | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func newTable(size int) *table { | |
| 77 | + | t := &table{} | |
| 78 | + | t.resize(size) | |
| 79 | + | return t | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // resize reallocates the slab, free list and index for size records and | |
| 83 | + | // reindexes the live ones. | |
| 84 | + | func (t *table) resize(size int) { | |
| 85 | + | old := t.recs | |
| 86 | + | t.recs = make([]record, size) | |
| 87 | + | copy(t.recs, old) | |
| 88 | + | t.free = make([]uint32, 0, size) | |
| 89 | + | for i := min(size, maxSlots) - 1; i >= 0; i-- { | |
| 90 | + | if i >= len(old) || old[i].kind == kindFree { | |
| 91 | + | t.free = append(t.free, uint32(i)) | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | t.index = make([]uint32, 2*size) | |
| 95 | + | t.shift = uint8(32 - bits.TrailingZeros(uint(len(t.index)))) | |
| 96 | + | t.reindex() | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // reindex rebuilds the index from the live records, dropping tombstones. | |
| 100 | + | // It does not allocate. | |
| 101 | + | func (t *table) reindex() { | |
| 102 | + | clear(t.index) | |
| 103 | + | t.tombs = 0 | |
| 104 | + | for i := range t.recs { | |
| 105 | + | if t.recs[i].kind != kindFree { | |
| 106 | + | t.indexAdd(t.recs[i].xid, uint32(i)+1) | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func (t *table) hash(xid uint32) uint32 { return (xid * 0x9E3779B1) >> t.shift } | |
| 112 | + | ||
| 113 | + | // indexAdd places slot for xid, which must be absent, in the first empty or | |
| 114 | + | // tombstone position of its probe sequence. | |
| 115 | + | func (t *table) indexAdd(xid, slot uint32) { | |
| 116 | + | mask := uint32(len(t.index) - 1) | |
| 117 | + | for i := t.hash(xid); ; i = (i + 1) & mask { | |
| 118 | + | switch t.index[i] { | |
| 119 | + | case 0: | |
| 120 | + | t.index[i] = slot | |
| 121 | + | return | |
| 122 | + | case indexGone: | |
| 123 | + | t.index[i] = slot | |
| 124 | + | t.tombs-- | |
| 125 | + | return | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | // grow doubles the table. It reports false at maxSlots. | |
| 131 | + | func (t *table) grow() bool { | |
| 132 | + | if len(t.recs) >= 1<<16 { | |
| 133 | + | return false | |
| 134 | + | } | |
| 135 | + | t.resize(2 * len(t.recs)) | |
| 136 | + | return len(t.free) > 0 | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // insert returns the slot of xid, creating a managed record (generation | |
| 140 | + | // bumped) when absent. It returns 0 when the table is full. Growth is the | |
| 141 | + | // only allocation. | |
| 142 | + | func (t *table) insert(xid uint32) Slot { | |
| 143 | + | if s := t.lookup(xid); s != 0 { | |
| 144 | + | return s | |
| 145 | + | } | |
| 146 | + | if len(t.free) == 0 && !t.grow() { | |
| 147 | + | return 0 | |
| 148 | + | } | |
| 149 | + | i := t.free[len(t.free)-1] | |
| 150 | + | t.free = t.free[:len(t.free)-1] | |
| 151 | + | if t.n+t.tombs+1 > len(t.index)/4*3 { // keep an empty index cell for lookup to stop at | |
| 152 | + | t.reindex() | |
| 153 | + | } | |
| 154 | + | r := &t.recs[i] | |
| 155 | + | gen := r.gen + 1 | |
| 156 | + | if gen == 0 { | |
| 157 | + | gen = 1 | |
| 158 | + | } | |
| 159 | + | *r = record{xid: xid, kind: kindManaged, gen: gen, input: true} | |
| 160 | + | t.indexAdd(xid, i+1) | |
| 161 | + | t.n++ | |
| 162 | + | return Slot(i + 1) | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | // remove frees s. The record is zeroed except its generation. | |
| 166 | + | func (t *table) remove(s Slot) { | |
| 167 | + | r := t.rec(s) | |
| 168 | + | if r == nil || r.kind == kindFree { | |
| 169 | + | return | |
| 170 | + | } | |
| 171 | + | mask := uint32(len(t.index) - 1) | |
| 172 | + | for i := t.hash(r.xid); ; i = (i + 1) & mask { | |
| 173 | + | v := t.index[i] | |
| 174 | + | if v == 0 { | |
| 175 | + | break | |
| 176 | + | } | |
| 177 | + | if v == uint32(s) { | |
| 178 | + | t.index[i] = indexGone | |
| 179 | + | t.tombs++ | |
| 180 | + | break | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | *r = record{gen: r.gen} | |
| 184 | + | t.free = append(t.free, uint32(s)-1) | |
| 185 | + | t.n-- | |
| 186 | + | if t.n+t.tombs > len(t.index)/4*3 { | |
| 187 | + | t.reindex() | |
| 188 | + | } | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | // lookup returns the slot of xid, or 0. | |
| 192 | + | func (t *table) lookup(xid uint32) Slot { | |
| 193 | + | mask := uint32(len(t.index) - 1) | |
| 194 | + | for i := t.hash(xid); ; i = (i + 1) & mask { | |
| 195 | + | v := t.index[i] | |
| 196 | + | if v == 0 { | |
| 197 | + | return 0 | |
| 198 | + | } | |
| 199 | + | if v != indexGone && t.recs[v-1].xid == xid { | |
| 200 | + | return Slot(v) | |
| 201 | + | } | |
| 202 | + | } | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | // rec returns the record of s, or nil when s is out of range. The pointer is | |
| 206 | + | // valid only until the next insert, host call or anything else that can grow | |
| 207 | + | // the table or free s: never hold it across those, fetch it again with rec(s). | |
| 208 | + | func (t *table) rec(s Slot) *record { | |
| 209 | + | if s == 0 || int(s) > len(t.recs) { | |
| 210 | + | return nil | |
| 211 | + | } | |
| 212 | + | return &t.recs[s-1] | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | // tag is the Expect argument naming s: slot in the low 16 bits, generation | |
| 216 | + | // in the high 16. | |
| 217 | + | func (t *table) tag(s Slot) uint32 { return uint32(s) | uint32(t.recs[s-1].gen)<<16 } | |
| 218 | + | ||
| 219 | + | // live resolves an Expect tag. It returns nil when the slot was freed or | |
| 220 | + | // reused since the request was sent. | |
| 221 | + | func (t *table) live(tag uint32) (Slot, *record) { | |
| 222 | + | s := Slot(tag & 0xFFFF) | |
| 223 | + | r := t.rec(s) | |
| 224 | + | if r == nil || r.kind == kindFree || r.gen != uint16(tag>>16) { | |
| 225 | + | return 0, nil | |
| 226 | + | } | |
| 227 | + | return s, r | |
| 228 | + | } | |
A internal/adapters/wayland/xwm/table_test.go +138 -0
| @@ -0,0 +1,138 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "math/rand/v2" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func TestTableAgainstMap(t *testing.T) { | |
| 11 | + | rng := rand.New(rand.NewPCG(1, 2)) | |
| 12 | + | tab := newTable(initialSlots) | |
| 13 | + | model := map[uint32]Slot{} | |
| 14 | + | var xids []uint32 | |
| 15 | + | for i := 0; i < 10000; i++ { | |
| 16 | + | switch op := rng.IntN(3); { | |
| 17 | + | case op < 2 || len(xids) == 0: | |
| 18 | + | xid := rng.Uint32()&0x1fffff | 0x200000 | |
| 19 | + | if _, ok := model[xid]; ok { | |
| 20 | + | continue | |
| 21 | + | } | |
| 22 | + | s := tab.insert(xid) | |
| 23 | + | require.NotZero(t, s) | |
| 24 | + | model[xid] = s | |
| 25 | + | xids = append(xids, xid) | |
| 26 | + | default: | |
| 27 | + | k := rng.IntN(len(xids)) | |
| 28 | + | xid := xids[k] | |
| 29 | + | xids[k] = xids[len(xids)-1] | |
| 30 | + | xids = xids[:len(xids)-1] | |
| 31 | + | tab.remove(model[xid]) | |
| 32 | + | delete(model, xid) | |
| 33 | + | } | |
| 34 | + | if i%500 == 0 { | |
| 35 | + | for xid, s := range model { | |
| 36 | + | require.Equal(t, s, tab.lookup(xid)) | |
| 37 | + | } | |
| 38 | + | require.Zero(t, tab.lookup(rng.Uint32()|1<<31)) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | require.Equal(t, len(model), tab.n) | |
| 42 | + | for xid, s := range model { | |
| 43 | + | require.Equal(t, s, tab.lookup(xid)) | |
| 44 | + | require.Equal(t, xid, tab.rec(s).xid) | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | func TestTableGrowth(t *testing.T) { | |
| 49 | + | tab := newTable(initialSlots) | |
| 50 | + | slots := make([]Slot, 0, initialSlots+1) | |
| 51 | + | for i := 0; i < initialSlots; i++ { | |
| 52 | + | slots = append(slots, tab.insert(uint32(0x200000+i))) | |
| 53 | + | } | |
| 54 | + | require.Len(t, tab.recs, initialSlots) | |
| 55 | + | tab.rec(slots[7]).title = "kept" | |
| 56 | + | slots = append(slots, tab.insert(0x300000)) | |
| 57 | + | require.Len(t, tab.recs, 2*initialSlots) | |
| 58 | + | require.Len(t, tab.index, 4*initialSlots) | |
| 59 | + | for i, s := range slots[:initialSlots] { | |
| 60 | + | require.Equal(t, s, tab.lookup(uint32(0x200000+i))) | |
| 61 | + | } | |
| 62 | + | require.Equal(t, "kept", tab.rec(slots[7]).title, "growth keeps records") | |
| 63 | + | require.Equal(t, slots[initialSlots], tab.lookup(0x300000)) | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func TestTableTombstoneReuse(t *testing.T) { | |
| 67 | + | tab := newTable(initialSlots) | |
| 68 | + | for i := 0; i < 5000; i++ { // far more churn than slots: tombstones must be recycled | |
| 69 | + | s := tab.insert(uint32(0x200000 + i)) | |
| 70 | + | require.NotZero(t, s) | |
| 71 | + | require.Equal(t, s, tab.lookup(uint32(0x200000+i))) | |
| 72 | + | tab.remove(s) | |
| 73 | + | require.Zero(t, tab.lookup(uint32(0x200000+i))) | |
| 74 | + | } | |
| 75 | + | require.Len(t, tab.recs, initialSlots) | |
| 76 | + | require.Zero(t, tab.n) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func TestTableGenerationAndTag(t *testing.T) { | |
| 80 | + | tab := newTable(initialSlots) | |
| 81 | + | s := tab.insert(0x200001) | |
| 82 | + | tag := tab.tag(s) | |
| 83 | + | got, rec := tab.live(tag) | |
| 84 | + | require.Equal(t, s, got) | |
| 85 | + | require.NotNil(t, rec) | |
| 86 | + | tab.remove(s) | |
| 87 | + | _, rec = tab.live(tag) | |
| 88 | + | require.Nil(t, rec, "freed slot") | |
| 89 | + | s2 := tab.insert(0x200002) | |
| 90 | + | require.Equal(t, s, s2, "LIFO free list reuses the slot") | |
| 91 | + | _, rec = tab.live(tag) | |
| 92 | + | require.Nil(t, rec, "stale tag after slot reuse") | |
| 93 | + | _, rec = tab.live(tab.tag(s2)) | |
| 94 | + | require.NotNil(t, rec) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | func TestTableFull(t *testing.T) { | |
| 98 | + | tab := newTable(initialSlots) | |
| 99 | + | for i := 0; i < maxSlots; i++ { | |
| 100 | + | require.NotZero(t, tab.insert(uint32(i+1)), i) | |
| 101 | + | } | |
| 102 | + | require.Zero(t, tab.insert(0xFFFFFF), "table is bounded by the 16-bit slot in Expect tags") | |
| 103 | + | require.Equal(t, Slot(maxSlots), tab.lookup(maxSlots)) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func TestTableInsertExisting(t *testing.T) { | |
| 107 | + | tab := newTable(initialSlots) | |
| 108 | + | s := tab.insert(5) | |
| 109 | + | require.Equal(t, s, tab.insert(5)) | |
| 110 | + | require.Equal(t, 1, tab.n) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func TestTableLookupAllocations(t *testing.T) { | |
| 114 | + | tab := newTable(initialSlots) | |
| 115 | + | for i := 0; i < 200; i++ { | |
| 116 | + | tab.insert(uint32(0x200000 + i)) | |
| 117 | + | } | |
| 118 | + | var sink Slot | |
| 119 | + | n := testing.AllocsPerRun(1000, func() { | |
| 120 | + | sink += tab.lookup(0x200010) | |
| 121 | + | sink += tab.lookup(0x7fffffff) // absent | |
| 122 | + | _, r := tab.live(tab.tag(3)) | |
| 123 | + | if r != nil { | |
| 124 | + | sink++ | |
| 125 | + | } | |
| 126 | + | }) | |
| 127 | + | require.Zero(t, n) | |
| 128 | + | require.NotZero(t, sink) | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | func TestTableInsertRemoveAllocations(t *testing.T) { | |
| 132 | + | tab := newTable(initialSlots) | |
| 133 | + | n := testing.AllocsPerRun(1000, func() { | |
| 134 | + | s := tab.insert(0x200001) | |
| 135 | + | tab.remove(s) | |
| 136 | + | }) | |
| 137 | + | require.Zero(t, n) | |
| 138 | + | } | |
A internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +950 -0
| @@ -0,0 +1,950 @@ | |||
| 1 | + | 1c00650001005000f30300000000000000000000000000000000000000000000 | |
| 2 | + | 0108660003000000ef0300000000000009000000000000000000000000000000 | |
| 3 | + | 6e6574207469746c650000001c00660001005000f30300000000000000000000 | |
| 4 | + | 0000000000000000000000000108670003000000ef0300000000000009000000 | |
| 5 | + | 0000000000000000000000006e6574207469746c650000001c00670001005000 | |
| 6 | + | f303000000000000000000000000000000000000000000000108680003000000 | |
| 7 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 8 | + | 650000001c00680001005000f303000000000000000000000000000000000000 | |
| 9 | + | 000000000108690003000000ef03000000000000090000000000000000000000 | |
| 10 | + | 000000006e6574207469746c650000001c00690001005000f303000000000000 | |
| 11 | + | 0000000000000000000000000000000001086a0003000000ef03000000000000 | |
| 12 | + | 090000000000000000000000000000006e6574207469746c650000001c006a00 | |
| 13 | + | 01005000f3030000000000000000000000000000000000000000000001086b00 | |
| 14 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 15 | + | 7469746c650000001c006b0001005000f3030000000000000000000000000000 | |
| 16 | + | 000000000000000001086c0003000000ef030000000000000900000000000000 | |
| 17 | + | 00000000000000006e6574207469746c650000001c006c0001005000f3030000 | |
| 18 | + | 000000000000000000000000000000000000000001086d0003000000ef030000 | |
| 19 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 20 | + | 1c006d0001005000f30300000000000000000000000000000000000000000000 | |
| 21 | + | 01086e0003000000ef0300000000000009000000000000000000000000000000 | |
| 22 | + | 6e6574207469746c650000001c006e0001005000f30300000000000000000000 | |
| 23 | + | 00000000000000000000000001086f0003000000ef0300000000000009000000 | |
| 24 | + | 0000000000000000000000006e6574207469746c650000001c006f0001005000 | |
| 25 | + | f303000000000000000000000000000000000000000000000108700003000000 | |
| 26 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 27 | + | 650000001c00700001005000f303000000000000000000000000000000000000 | |
| 28 | + | 000000000108710003000000ef03000000000000090000000000000000000000 | |
| 29 | + | 000000006e6574207469746c650000001c00710001005000f303000000000000 | |
| 30 | + | 000000000000000000000000000000000108720003000000ef03000000000000 | |
| 31 | + | 090000000000000000000000000000006e6574207469746c650000001c007200 | |
| 32 | + | 01005000f3030000000000000000000000000000000000000000000001087300 | |
| 33 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 34 | + | 7469746c650000001c00730001005000f3030000000000000000000000000000 | |
| 35 | + | 00000000000000000108740003000000ef030000000000000900000000000000 | |
| 36 | + | 00000000000000006e6574207469746c650000001c00740001005000f3030000 | |
| 37 | + | 00000000000000000000000000000000000000000108750003000000ef030000 | |
| 38 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 39 | + | 1c00750001005000f30300000000000000000000000000000000000000000000 | |
| 40 | + | 0108760003000000ef0300000000000009000000000000000000000000000000 | |
| 41 | + | 6e6574207469746c650000001c00760001005000f30300000000000000000000 | |
| 42 | + | 0000000000000000000000000108770003000000ef0300000000000009000000 | |
| 43 | + | 0000000000000000000000006e6574207469746c650000001c00770001005000 | |
| 44 | + | f303000000000000000000000000000000000000000000000108780003000000 | |
| 45 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 46 | + | 650000001c00780001005000f303000000000000000000000000000000000000 | |
| 47 | + | 000000000108790003000000ef03000000000000090000000000000000000000 | |
| 48 | + | 000000006e6574207469746c650000001c00790001005000f303000000000000 | |
| 49 | + | 0000000000000000000000000000000001087a0003000000ef03000000000000 | |
| 50 | + | 090000000000000000000000000000006e6574207469746c650000001c007a00 | |
| 51 | + | 01005000f3030000000000000000000000000000000000000000000001087b00 | |
| 52 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 53 | + | 7469746c650000001c007b0001005000f3030000000000000000000000000000 | |
| 54 | + | 000000000000000001087c0003000000ef030000000000000900000000000000 | |
| 55 | + | 00000000000000006e6574207469746c650000001c007c0001005000f3030000 | |
| 56 | + | 000000000000000000000000000000000000000001087d0003000000ef030000 | |
| 57 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 58 | + | 1c007d0001005000f30300000000000000000000000000000000000000000000 | |
| 59 | + | 01087e0003000000ef0300000000000009000000000000000000000000000000 | |
| 60 | + | 6e6574207469746c650000001c007e0001005000f30300000000000000000000 | |
| 61 | + | 00000000000000000000000001087f0003000000ef0300000000000009000000 | |
| 62 | + | 0000000000000000000000006e6574207469746c650000001c007f0001005000 | |
| 63 | + | f303000000000000000000000000000000000000000000000108800003000000 | |
| 64 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 65 | + | 650000001c00800001005000f303000000000000000000000000000000000000 | |
| 66 | + | 000000000108810003000000ef03000000000000090000000000000000000000 | |
| 67 | + | 000000006e6574207469746c650000001c00810001005000f303000000000000 | |
| 68 | + | 000000000000000000000000000000000108820003000000ef03000000000000 | |
| 69 | + | 090000000000000000000000000000006e6574207469746c650000001c008200 | |
| 70 | + | 01005000f3030000000000000000000000000000000000000000000001088300 | |
| 71 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 72 | + | 7469746c650000001c00830001005000f3030000000000000000000000000000 | |
| 73 | + | 00000000000000000108840003000000ef030000000000000900000000000000 | |
| 74 | + | 00000000000000006e6574207469746c650000001c00840001005000f3030000 | |
| 75 | + | 00000000000000000000000000000000000000000108850003000000ef030000 | |
| 76 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 77 | + | 1c00850001005000f30300000000000000000000000000000000000000000000 | |
| 78 | + | 0108860003000000ef0300000000000009000000000000000000000000000000 | |
| 79 | + | 6e6574207469746c650000001c00860001005000f30300000000000000000000 | |
| 80 | + | 0000000000000000000000000108870003000000ef0300000000000009000000 | |
| 81 | + | 0000000000000000000000006e6574207469746c650000001c00870001005000 | |
| 82 | + | f303000000000000000000000000000000000000000000000108880003000000 | |
| 83 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 84 | + | 650000001c00880001005000f303000000000000000000000000000000000000 | |
| 85 | + | 000000000108890003000000ef03000000000000090000000000000000000000 | |
| 86 | + | 000000006e6574207469746c650000001c00890001005000f303000000000000 | |
| 87 | + | 0000000000000000000000000000000001088a0003000000ef03000000000000 | |
| 88 | + | 090000000000000000000000000000006e6574207469746c650000001c008a00 | |
| 89 | + | 01005000f3030000000000000000000000000000000000000000000001088b00 | |
| 90 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 91 | + | 7469746c650000001c008b0001005000f3030000000000000000000000000000 | |
| 92 | + | 000000000000000001088c0003000000ef030000000000000900000000000000 | |
| 93 | + | 00000000000000006e6574207469746c650000001c008c0001005000f3030000 | |
| 94 | + | 000000000000000000000000000000000000000001088d0003000000ef030000 | |
| 95 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 96 | + | 1c008d0001005000f30300000000000000000000000000000000000000000000 | |
| 97 | + | 01088e0003000000ef0300000000000009000000000000000000000000000000 | |
| 98 | + | 6e6574207469746c650000001c008e0001005000f30300000000000000000000 | |
| 99 | + | 00000000000000000000000001088f0003000000ef0300000000000009000000 | |
| 100 | + | 0000000000000000000000006e6574207469746c650000001c008f0001005000 | |
| 101 | + | f303000000000000000000000000000000000000000000000108900003000000 | |
| 102 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 103 | + | 650000001c00900001005000f303000000000000000000000000000000000000 | |
| 104 | + | 000000000108910003000000ef03000000000000090000000000000000000000 | |
| 105 | + | 000000006e6574207469746c650000001c00910001005000f303000000000000 | |
| 106 | + | 000000000000000000000000000000000108920003000000ef03000000000000 | |
| 107 | + | 090000000000000000000000000000006e6574207469746c650000001c009200 | |
| 108 | + | 01005000f3030000000000000000000000000000000000000000000001089300 | |
| 109 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 110 | + | 7469746c650000001c00930001005000f3030000000000000000000000000000 | |
| 111 | + | 00000000000000000108940003000000ef030000000000000900000000000000 | |
| 112 | + | 00000000000000006e6574207469746c650000001c00940001005000f3030000 | |
| 113 | + | 00000000000000000000000000000000000000000108950003000000ef030000 | |
| 114 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 115 | + | 1c00950001005000f30300000000000000000000000000000000000000000000 | |
| 116 | + | 0108960003000000ef0300000000000009000000000000000000000000000000 | |
| 117 | + | 6e6574207469746c650000001c00960001005000f30300000000000000000000 | |
| 118 | + | 0000000000000000000000000108970003000000ef0300000000000009000000 | |
| 119 | + | 0000000000000000000000006e6574207469746c650000001c00970001005000 | |
| 120 | + | f303000000000000000000000000000000000000000000000108980003000000 | |
| 121 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 122 | + | 650000001c00980001005000f303000000000000000000000000000000000000 | |
| 123 | + | 000000000108990003000000ef03000000000000090000000000000000000000 | |
| 124 | + | 000000006e6574207469746c650000001c00990001005000f303000000000000 | |
| 125 | + | 0000000000000000000000000000000001089a0003000000ef03000000000000 | |
| 126 | + | 090000000000000000000000000000006e6574207469746c650000001c009a00 | |
| 127 | + | 01005000f3030000000000000000000000000000000000000000000001089b00 | |
| 128 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 129 | + | 7469746c650000001c009b0001005000f3030000000000000000000000000000 | |
| 130 | + | 000000000000000001089c0003000000ef030000000000000900000000000000 | |
| 131 | + | 00000000000000006e6574207469746c650000001c009c0001005000f3030000 | |
| 132 | + | 000000000000000000000000000000000000000001089d0003000000ef030000 | |
| 133 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 134 | + | 1c009d0001005000f30300000000000000000000000000000000000000000000 | |
| 135 | + | 01089e0003000000ef0300000000000009000000000000000000000000000000 | |
| 136 | + | 6e6574207469746c650000001c009e0001005000f30300000000000000000000 | |
| 137 | + | 00000000000000000000000001089f0003000000ef0300000000000009000000 | |
| 138 | + | 0000000000000000000000006e6574207469746c650000001c009f0001005000 | |
| 139 | + | f303000000000000000000000000000000000000000000000108a00003000000 | |
| 140 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 141 | + | 650000001c00a00001005000f303000000000000000000000000000000000000 | |
| 142 | + | 000000000108a10003000000ef03000000000000090000000000000000000000 | |
| 143 | + | 000000006e6574207469746c650000001c00a10001005000f303000000000000 | |
| 144 | + | 000000000000000000000000000000000108a20003000000ef03000000000000 | |
| 145 | + | 090000000000000000000000000000006e6574207469746c650000001c00a200 | |
| 146 | + | 01005000f303000000000000000000000000000000000000000000000108a300 | |
| 147 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 148 | + | 7469746c650000001c00a30001005000f3030000000000000000000000000000 | |
| 149 | + | 00000000000000000108a40003000000ef030000000000000900000000000000 | |
| 150 | + | 00000000000000006e6574207469746c650000001c00a40001005000f3030000 | |
| 151 | + | 00000000000000000000000000000000000000000108a50003000000ef030000 | |
| 152 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 153 | + | 1c00a50001005000f30300000000000000000000000000000000000000000000 | |
| 154 | + | 0108a60003000000ef0300000000000009000000000000000000000000000000 | |
| 155 | + | 6e6574207469746c650000001c00a60001005000f30300000000000000000000 | |
| 156 | + | 0000000000000000000000000108a70003000000ef0300000000000009000000 | |
| 157 | + | 0000000000000000000000006e6574207469746c650000001c00a70001005000 | |
| 158 | + | f303000000000000000000000000000000000000000000000108a80003000000 | |
| 159 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 160 | + | 650000001c00a80001005000f303000000000000000000000000000000000000 | |
| 161 | + | 000000000108a90003000000ef03000000000000090000000000000000000000 | |
| 162 | + | 000000006e6574207469746c650000001c00a90001005000f303000000000000 | |
| 163 | + | 000000000000000000000000000000000108aa0003000000ef03000000000000 | |
| 164 | + | 090000000000000000000000000000006e6574207469746c650000001c00aa00 | |
| 165 | + | 01005000f303000000000000000000000000000000000000000000000108ab00 | |
| 166 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 167 | + | 7469746c650000001c00ab0001005000f3030000000000000000000000000000 | |
| 168 | + | 00000000000000000108ac0003000000ef030000000000000900000000000000 | |
| 169 | + | 00000000000000006e6574207469746c650000001c00ac0001005000f3030000 | |
| 170 | + | 00000000000000000000000000000000000000000108ad0003000000ef030000 | |
| 171 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 172 | + | 1c00ad0001005000f30300000000000000000000000000000000000000000000 | |
| 173 | + | 0108ae0003000000ef0300000000000009000000000000000000000000000000 | |
| 174 | + | 6e6574207469746c650000001c00ae0001005000f30300000000000000000000 | |
| 175 | + | 0000000000000000000000000108af0003000000ef0300000000000009000000 | |
| 176 | + | 0000000000000000000000006e6574207469746c650000001c00af0001005000 | |
| 177 | + | f303000000000000000000000000000000000000000000000108b00003000000 | |
| 178 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 179 | + | 650000001c00b00001005000f303000000000000000000000000000000000000 | |
| 180 | + | 000000000108b10003000000ef03000000000000090000000000000000000000 | |
| 181 | + | 000000006e6574207469746c650000001c00b10001005000f303000000000000 | |
| 182 | + | 000000000000000000000000000000000108b20003000000ef03000000000000 | |
| 183 | + | 090000000000000000000000000000006e6574207469746c650000001c00b200 | |
| 184 | + | 01005000f303000000000000000000000000000000000000000000000108b300 | |
| 185 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 186 | + | 7469746c650000001c00b30001005000f3030000000000000000000000000000 | |
| 187 | + | 00000000000000000108b40003000000ef030000000000000900000000000000 | |
| 188 | + | 00000000000000006e6574207469746c650000001c00b40001005000f3030000 | |
| 189 | + | 00000000000000000000000000000000000000000108b50003000000ef030000 | |
| 190 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 191 | + | 1c00b50001005000f30300000000000000000000000000000000000000000000 | |
| 192 | + | 0108b60003000000ef0300000000000009000000000000000000000000000000 | |
| 193 | + | 6e6574207469746c650000001c00b60001005000f30300000000000000000000 | |
| 194 | + | 0000000000000000000000000108b70003000000ef0300000000000009000000 | |
| 195 | + | 0000000000000000000000006e6574207469746c650000001c00b70001005000 | |
| 196 | + | f303000000000000000000000000000000000000000000000108b80003000000 | |
| 197 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 198 | + | 650000001c00b80001005000f303000000000000000000000000000000000000 | |
| 199 | + | 000000000108b90003000000ef03000000000000090000000000000000000000 | |
| 200 | + | 000000006e6574207469746c650000001c00b90001005000f303000000000000 | |
| 201 | + | 000000000000000000000000000000000108ba0003000000ef03000000000000 | |
| 202 | + | 090000000000000000000000000000006e6574207469746c650000001c00ba00 | |
| 203 | + | 01005000f303000000000000000000000000000000000000000000000108bb00 | |
| 204 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 205 | + | 7469746c650000001c00bb0001005000f3030000000000000000000000000000 | |
| 206 | + | 00000000000000000108bc0003000000ef030000000000000900000000000000 | |
| 207 | + | 00000000000000006e6574207469746c650000001c00bc0001005000f3030000 | |
| 208 | + | 00000000000000000000000000000000000000000108bd0003000000ef030000 | |
| 209 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 210 | + | 1c00bd0001005000f30300000000000000000000000000000000000000000000 | |
| 211 | + | 0108be0003000000ef0300000000000009000000000000000000000000000000 | |
| 212 | + | 6e6574207469746c650000001c00be0001005000f30300000000000000000000 | |
| 213 | + | 0000000000000000000000000108bf0003000000ef0300000000000009000000 | |
| 214 | + | 0000000000000000000000006e6574207469746c650000001c00bf0001005000 | |
| 215 | + | f303000000000000000000000000000000000000000000000108c00003000000 | |
| 216 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 217 | + | 650000001c00c00001005000f303000000000000000000000000000000000000 | |
| 218 | + | 000000000108c10003000000ef03000000000000090000000000000000000000 | |
| 219 | + | 000000006e6574207469746c650000001c00c10001005000f303000000000000 | |
| 220 | + | 000000000000000000000000000000000108c20003000000ef03000000000000 | |
| 221 | + | 090000000000000000000000000000006e6574207469746c650000001c00c200 | |
| 222 | + | 01005000f303000000000000000000000000000000000000000000000108c300 | |
| 223 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 224 | + | 7469746c650000001c00c30001005000f3030000000000000000000000000000 | |
| 225 | + | 00000000000000000108c40003000000ef030000000000000900000000000000 | |
| 226 | + | 00000000000000006e6574207469746c650000001c00c40001005000f3030000 | |
| 227 | + | 00000000000000000000000000000000000000000108c50003000000ef030000 | |
| 228 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 229 | + | 1c00c50001005000f30300000000000000000000000000000000000000000000 | |
| 230 | + | 0108c60003000000ef0300000000000009000000000000000000000000000000 | |
| 231 | + | 6e6574207469746c650000001c00c60001005000f30300000000000000000000 | |
| 232 | + | 0000000000000000000000000108c70003000000ef0300000000000009000000 | |
| 233 | + | 0000000000000000000000006e6574207469746c650000001c00c70001005000 | |
| 234 | + | f303000000000000000000000000000000000000000000000108c80003000000 | |
| 235 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 236 | + | 650000001c00c80001005000f303000000000000000000000000000000000000 | |
| 237 | + | 000000000108c90003000000ef03000000000000090000000000000000000000 | |
| 238 | + | 000000006e6574207469746c650000001c00c90001005000f303000000000000 | |
| 239 | + | 000000000000000000000000000000000108ca0003000000ef03000000000000 | |
| 240 | + | 090000000000000000000000000000006e6574207469746c650000001c00ca00 | |
| 241 | + | 01005000f303000000000000000000000000000000000000000000000108cb00 | |
| 242 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 243 | + | 7469746c650000001c00cb0001005000f3030000000000000000000000000000 | |
| 244 | + | 00000000000000000108cc0003000000ef030000000000000900000000000000 | |
| 245 | + | 00000000000000006e6574207469746c650000001c00cc0001005000f3030000 | |
| 246 | + | 00000000000000000000000000000000000000000108cd0003000000ef030000 | |
| 247 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 248 | + | 1c00cd0001005000f30300000000000000000000000000000000000000000000 | |
| 249 | + | 0108ce0003000000ef0300000000000009000000000000000000000000000000 | |
| 250 | + | 6e6574207469746c650000001c00ce0001005000f30300000000000000000000 | |
| 251 | + | 0000000000000000000000000108cf0003000000ef0300000000000009000000 | |
| 252 | + | 0000000000000000000000006e6574207469746c650000001c00cf0001005000 | |
| 253 | + | f303000000000000000000000000000000000000000000000108d00003000000 | |
| 254 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 255 | + | 650000001c00d00001005000f303000000000000000000000000000000000000 | |
| 256 | + | 000000000108d10003000000ef03000000000000090000000000000000000000 | |
| 257 | + | 000000006e6574207469746c650000001c00d10001005000f303000000000000 | |
| 258 | + | 000000000000000000000000000000000108d20003000000ef03000000000000 | |
| 259 | + | 090000000000000000000000000000006e6574207469746c650000001c00d200 | |
| 260 | + | 01005000f303000000000000000000000000000000000000000000000108d300 | |
| 261 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 262 | + | 7469746c650000001c00d30001005000f3030000000000000000000000000000 | |
| 263 | + | 00000000000000000108d40003000000ef030000000000000900000000000000 | |
| 264 | + | 00000000000000006e6574207469746c650000001c00d40001005000f3030000 | |
| 265 | + | 00000000000000000000000000000000000000000108d50003000000ef030000 | |
| 266 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 267 | + | 1c00d50001005000f30300000000000000000000000000000000000000000000 | |
| 268 | + | 0108d60003000000ef0300000000000009000000000000000000000000000000 | |
| 269 | + | 6e6574207469746c650000001c00d60001005000f30300000000000000000000 | |
| 270 | + | 0000000000000000000000000108d70003000000ef0300000000000009000000 | |
| 271 | + | 0000000000000000000000006e6574207469746c650000001c00d70001005000 | |
| 272 | + | f303000000000000000000000000000000000000000000000108d80003000000 | |
| 273 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 274 | + | 650000001c00d80001005000f303000000000000000000000000000000000000 | |
| 275 | + | 000000000108d90003000000ef03000000000000090000000000000000000000 | |
| 276 | + | 000000006e6574207469746c650000001c00d90001005000f303000000000000 | |
| 277 | + | 000000000000000000000000000000000108da0003000000ef03000000000000 | |
| 278 | + | 090000000000000000000000000000006e6574207469746c650000001c00da00 | |
| 279 | + | 01005000f303000000000000000000000000000000000000000000000108db00 | |
| 280 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 281 | + | 7469746c650000001c00db0001005000f3030000000000000000000000000000 | |
| 282 | + | 00000000000000000108dc0003000000ef030000000000000900000000000000 | |
| 283 | + | 00000000000000006e6574207469746c650000001c00dc0001005000f3030000 | |
| 284 | + | 00000000000000000000000000000000000000000108dd0003000000ef030000 | |
| 285 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 286 | + | 1c00dd0001005000f30300000000000000000000000000000000000000000000 | |
| 287 | + | 0108de0003000000ef0300000000000009000000000000000000000000000000 | |
| 288 | + | 6e6574207469746c650000001c00de0001005000f30300000000000000000000 | |
| 289 | + | 0000000000000000000000000108df0003000000ef0300000000000009000000 | |
| 290 | + | 0000000000000000000000006e6574207469746c650000001c00df0001005000 | |
| 291 | + | f303000000000000000000000000000000000000000000000108e00003000000 | |
| 292 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 293 | + | 650000001c00e00001005000f303000000000000000000000000000000000000 | |
| 294 | + | 000000000108e10003000000ef03000000000000090000000000000000000000 | |
| 295 | + | 000000006e6574207469746c650000001c00e10001005000f303000000000000 | |
| 296 | + | 000000000000000000000000000000000108e20003000000ef03000000000000 | |
| 297 | + | 090000000000000000000000000000006e6574207469746c650000001c00e200 | |
| 298 | + | 01005000f303000000000000000000000000000000000000000000000108e300 | |
| 299 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 300 | + | 7469746c650000001c00e30001005000f3030000000000000000000000000000 | |
| 301 | + | 00000000000000000108e40003000000ef030000000000000900000000000000 | |
| 302 | + | 00000000000000006e6574207469746c650000001c00e40001005000f3030000 | |
| 303 | + | 00000000000000000000000000000000000000000108e50003000000ef030000 | |
| 304 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 305 | + | 1c00e50001005000f30300000000000000000000000000000000000000000000 | |
| 306 | + | 0108e60003000000ef0300000000000009000000000000000000000000000000 | |
| 307 | + | 6e6574207469746c650000001c00e60001005000f30300000000000000000000 | |
| 308 | + | 0000000000000000000000000108e70003000000ef0300000000000009000000 | |
| 309 | + | 0000000000000000000000006e6574207469746c650000001c00e70001005000 | |
| 310 | + | f303000000000000000000000000000000000000000000000108e80003000000 | |
| 311 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 312 | + | 650000001c00e80001005000f303000000000000000000000000000000000000 | |
| 313 | + | 000000000108e90003000000ef03000000000000090000000000000000000000 | |
| 314 | + | 000000006e6574207469746c650000001c00e90001005000f303000000000000 | |
| 315 | + | 000000000000000000000000000000000108ea0003000000ef03000000000000 | |
| 316 | + | 090000000000000000000000000000006e6574207469746c650000001c00ea00 | |
| 317 | + | 01005000f303000000000000000000000000000000000000000000000108eb00 | |
| 318 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 319 | + | 7469746c650000001c00eb0001005000f3030000000000000000000000000000 | |
| 320 | + | 00000000000000000108ec0003000000ef030000000000000900000000000000 | |
| 321 | + | 00000000000000006e6574207469746c650000001c00ec0001005000f3030000 | |
| 322 | + | 00000000000000000000000000000000000000000108ed0003000000ef030000 | |
| 323 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 324 | + | 1c00ed0001005000f30300000000000000000000000000000000000000000000 | |
| 325 | + | 0108ee0003000000ef0300000000000009000000000000000000000000000000 | |
| 326 | + | 6e6574207469746c650000001c00ee0001005000f30300000000000000000000 | |
| 327 | + | 0000000000000000000000000108ef0003000000ef0300000000000009000000 | |
| 328 | + | 0000000000000000000000006e6574207469746c650000001c00ef0001005000 | |
| 329 | + | f303000000000000000000000000000000000000000000000108f00003000000 | |
| 330 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 331 | + | 650000001c00f00001005000f303000000000000000000000000000000000000 | |
| 332 | + | 000000000108f10003000000ef03000000000000090000000000000000000000 | |
| 333 | + | 000000006e6574207469746c650000001c00f10001005000f303000000000000 | |
| 334 | + | 000000000000000000000000000000000108f20003000000ef03000000000000 | |
| 335 | + | 090000000000000000000000000000006e6574207469746c650000001c00f200 | |
| 336 | + | 01005000f303000000000000000000000000000000000000000000000108f300 | |
| 337 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 338 | + | 7469746c650000001c00f30001005000f3030000000000000000000000000000 | |
| 339 | + | 00000000000000000108f40003000000ef030000000000000900000000000000 | |
| 340 | + | 00000000000000006e6574207469746c650000001c00f40001005000f3030000 | |
| 341 | + | 00000000000000000000000000000000000000000108f50003000000ef030000 | |
| 342 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 343 | + | 1c00f50001005000f30300000000000000000000000000000000000000000000 | |
| 344 | + | 0108f60003000000ef0300000000000009000000000000000000000000000000 | |
| 345 | + | 6e6574207469746c650000001c00f60001005000f30300000000000000000000 | |
| 346 | + | 0000000000000000000000000108f70003000000ef0300000000000009000000 | |
| 347 | + | 0000000000000000000000006e6574207469746c650000001c00f70001005000 | |
| 348 | + | f303000000000000000000000000000000000000000000000108f80003000000 | |
| 349 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 350 | + | 650000001c00f80001005000f303000000000000000000000000000000000000 | |
| 351 | + | 000000000108f90003000000ef03000000000000090000000000000000000000 | |
| 352 | + | 000000006e6574207469746c650000001c00f90001005000f303000000000000 | |
| 353 | + | 000000000000000000000000000000000108fa0003000000ef03000000000000 | |
| 354 | + | 090000000000000000000000000000006e6574207469746c650000001c00fa00 | |
| 355 | + | 01005000f303000000000000000000000000000000000000000000000108fb00 | |
| 356 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 357 | + | 7469746c650000001c00fb0001005000f3030000000000000000000000000000 | |
| 358 | + | 00000000000000000108fc0003000000ef030000000000000900000000000000 | |
| 359 | + | 00000000000000006e6574207469746c650000001c00fc0001005000f3030000 | |
| 360 | + | 00000000000000000000000000000000000000000108fd0003000000ef030000 | |
| 361 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 362 | + | 1c00fd0001005000f30300000000000000000000000000000000000000000000 | |
| 363 | + | 0108fe0003000000ef0300000000000009000000000000000000000000000000 | |
| 364 | + | 6e6574207469746c650000001c00fe0001005000f30300000000000000000000 | |
| 365 | + | 0000000000000000000000000108ff0003000000ef0300000000000009000000 | |
| 366 | + | 0000000000000000000000006e6574207469746c650000001c00ff0001005000 | |
| 367 | + | f303000000000000000000000000000000000000000000000108000103000000 | |
| 368 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 369 | + | 650000001c00000101005000f303000000000000000000000000000000000000 | |
| 370 | + | 000000000108010103000000ef03000000000000090000000000000000000000 | |
| 371 | + | 000000006e6574207469746c650000001c00010101005000f303000000000000 | |
| 372 | + | 000000000000000000000000000000000108020103000000ef03000000000000 | |
| 373 | + | 090000000000000000000000000000006e6574207469746c650000001c000201 | |
| 374 | + | 01005000f3030000000000000000000000000000000000000000000001080301 | |
| 375 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 376 | + | 7469746c650000001c00030101005000f3030000000000000000000000000000 | |
| 377 | + | 00000000000000000108040103000000ef030000000000000900000000000000 | |
| 378 | + | 00000000000000006e6574207469746c650000001c00040101005000f3030000 | |
| 379 | + | 00000000000000000000000000000000000000000108050103000000ef030000 | |
| 380 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 381 | + | 1c00050101005000f30300000000000000000000000000000000000000000000 | |
| 382 | + | 0108060103000000ef0300000000000009000000000000000000000000000000 | |
| 383 | + | 6e6574207469746c650000001c00060101005000f30300000000000000000000 | |
| 384 | + | 0000000000000000000000000108070103000000ef0300000000000009000000 | |
| 385 | + | 0000000000000000000000006e6574207469746c650000001c00070101005000 | |
| 386 | + | f303000000000000000000000000000000000000000000000108080103000000 | |
| 387 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 388 | + | 650000001c00080101005000f303000000000000000000000000000000000000 | |
| 389 | + | 000000000108090103000000ef03000000000000090000000000000000000000 | |
| 390 | + | 000000006e6574207469746c650000001c00090101005000f303000000000000 | |
| 391 | + | 0000000000000000000000000000000001080a0103000000ef03000000000000 | |
| 392 | + | 090000000000000000000000000000006e6574207469746c650000001c000a01 | |
| 393 | + | 01005000f3030000000000000000000000000000000000000000000001080b01 | |
| 394 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 395 | + | 7469746c650000001c000b0101005000f3030000000000000000000000000000 | |
| 396 | + | 000000000000000001080c0103000000ef030000000000000900000000000000 | |
| 397 | + | 00000000000000006e6574207469746c650000001c000c0101005000f3030000 | |
| 398 | + | 000000000000000000000000000000000000000001080d0103000000ef030000 | |
| 399 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 400 | + | 1c000d0101005000f30300000000000000000000000000000000000000000000 | |
| 401 | + | 01080e0103000000ef0300000000000009000000000000000000000000000000 | |
| 402 | + | 6e6574207469746c650000001c000e0101005000f30300000000000000000000 | |
| 403 | + | 00000000000000000000000001080f0103000000ef0300000000000009000000 | |
| 404 | + | 0000000000000000000000006e6574207469746c650000001c000f0101005000 | |
| 405 | + | f303000000000000000000000000000000000000000000000108100103000000 | |
| 406 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 407 | + | 650000001c00100101005000f303000000000000000000000000000000000000 | |
| 408 | + | 000000000108110103000000ef03000000000000090000000000000000000000 | |
| 409 | + | 000000006e6574207469746c650000001c00110101005000f303000000000000 | |
| 410 | + | 000000000000000000000000000000000108120103000000ef03000000000000 | |
| 411 | + | 090000000000000000000000000000006e6574207469746c650000001c001201 | |
| 412 | + | 01005000f3030000000000000000000000000000000000000000000001081301 | |
| 413 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 414 | + | 7469746c650000001c00130101005000f3030000000000000000000000000000 | |
| 415 | + | 00000000000000000108140103000000ef030000000000000900000000000000 | |
| 416 | + | 00000000000000006e6574207469746c650000001c00140101005000f3030000 | |
| 417 | + | 00000000000000000000000000000000000000000108150103000000ef030000 | |
| 418 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 419 | + | 1c00150101005000f30300000000000000000000000000000000000000000000 | |
| 420 | + | 0108160103000000ef0300000000000009000000000000000000000000000000 | |
| 421 | + | 6e6574207469746c650000001c00160101005000f30300000000000000000000 | |
| 422 | + | 0000000000000000000000000108170103000000ef0300000000000009000000 | |
| 423 | + | 0000000000000000000000006e6574207469746c650000001c00170101005000 | |
| 424 | + | f303000000000000000000000000000000000000000000000108180103000000 | |
| 425 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 426 | + | 650000001c00180101005000f303000000000000000000000000000000000000 | |
| 427 | + | 000000000108190103000000ef03000000000000090000000000000000000000 | |
| 428 | + | 000000006e6574207469746c650000001c00190101005000f303000000000000 | |
| 429 | + | 0000000000000000000000000000000001081a0103000000ef03000000000000 | |
| 430 | + | 090000000000000000000000000000006e6574207469746c650000001c001a01 | |
| 431 | + | 01005000f3030000000000000000000000000000000000000000000001081b01 | |
| 432 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 433 | + | 7469746c650000001c001b0101005000f3030000000000000000000000000000 | |
| 434 | + | 000000000000000001081c0103000000ef030000000000000900000000000000 | |
| 435 | + | 00000000000000006e6574207469746c650000001c001c0101005000f3030000 | |
| 436 | + | 000000000000000000000000000000000000000001081d0103000000ef030000 | |
| 437 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 438 | + | 1c001d0101005000f30300000000000000000000000000000000000000000000 | |
| 439 | + | 01081e0103000000ef0300000000000009000000000000000000000000000000 | |
| 440 | + | 6e6574207469746c650000001c001e0101005000f30300000000000000000000 | |
| 441 | + | 00000000000000000000000001081f0103000000ef0300000000000009000000 | |
| 442 | + | 0000000000000000000000006e6574207469746c650000001c001f0101005000 | |
| 443 | + | f303000000000000000000000000000000000000000000000108200103000000 | |
| 444 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 445 | + | 650000001c00200101005000f303000000000000000000000000000000000000 | |
| 446 | + | 000000000108210103000000ef03000000000000090000000000000000000000 | |
| 447 | + | 000000006e6574207469746c650000001c00210101005000f303000000000000 | |
| 448 | + | 000000000000000000000000000000000108220103000000ef03000000000000 | |
| 449 | + | 090000000000000000000000000000006e6574207469746c650000001c002201 | |
| 450 | + | 01005000f3030000000000000000000000000000000000000000000001082301 | |
| 451 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 452 | + | 7469746c650000001c00230101005000f3030000000000000000000000000000 | |
| 453 | + | 00000000000000000108240103000000ef030000000000000900000000000000 | |
| 454 | + | 00000000000000006e6574207469746c650000001c00240101005000f3030000 | |
| 455 | + | 00000000000000000000000000000000000000000108250103000000ef030000 | |
| 456 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 457 | + | 1c00250101005000f30300000000000000000000000000000000000000000000 | |
| 458 | + | 0108260103000000ef0300000000000009000000000000000000000000000000 | |
| 459 | + | 6e6574207469746c650000001c00260101005000f30300000000000000000000 | |
| 460 | + | 0000000000000000000000000108270103000000ef0300000000000009000000 | |
| 461 | + | 0000000000000000000000006e6574207469746c650000001c00270101005000 | |
| 462 | + | f303000000000000000000000000000000000000000000000108280103000000 | |
| 463 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 464 | + | 650000001c00280101005000f303000000000000000000000000000000000000 | |
| 465 | + | 000000000108290103000000ef03000000000000090000000000000000000000 | |
| 466 | + | 000000006e6574207469746c650000001c00290101005000f303000000000000 | |
| 467 | + | 0000000000000000000000000000000001082a0103000000ef03000000000000 | |
| 468 | + | 090000000000000000000000000000006e6574207469746c650000001c002a01 | |
| 469 | + | 01005000f3030000000000000000000000000000000000000000000001082b01 | |
| 470 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 471 | + | 7469746c650000001c002b0101005000f3030000000000000000000000000000 | |
| 472 | + | 000000000000000001082c0103000000ef030000000000000900000000000000 | |
| 473 | + | 00000000000000006e6574207469746c650000001c002c0101005000f3030000 | |
| 474 | + | 000000000000000000000000000000000000000001082d0103000000ef030000 | |
| 475 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 476 | + | 1c002d0101005000f30300000000000000000000000000000000000000000000 | |
| 477 | + | 01082e0103000000ef0300000000000009000000000000000000000000000000 | |
| 478 | + | 6e6574207469746c650000001c002e0101005000f30300000000000000000000 | |
| 479 | + | 00000000000000000000000001082f0103000000ef0300000000000009000000 | |
| 480 | + | 0000000000000000000000006e6574207469746c650000001c002f0101005000 | |
| 481 | + | f303000000000000000000000000000000000000000000000108300103000000 | |
| 482 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 483 | + | 650000001c00300101005000f303000000000000000000000000000000000000 | |
| 484 | + | 000000000108310103000000ef03000000000000090000000000000000000000 | |
| 485 | + | 000000006e6574207469746c650000001c00310101005000f303000000000000 | |
| 486 | + | 000000000000000000000000000000000108320103000000ef03000000000000 | |
| 487 | + | 090000000000000000000000000000006e6574207469746c650000001c003201 | |
| 488 | + | 01005000f3030000000000000000000000000000000000000000000001083301 | |
| 489 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 490 | + | 7469746c650000001c00330101005000f3030000000000000000000000000000 | |
| 491 | + | 00000000000000000108340103000000ef030000000000000900000000000000 | |
| 492 | + | 00000000000000006e6574207469746c650000001c00340101005000f3030000 | |
| 493 | + | 00000000000000000000000000000000000000000108350103000000ef030000 | |
| 494 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 495 | + | 1c00350101005000f30300000000000000000000000000000000000000000000 | |
| 496 | + | 0108360103000000ef0300000000000009000000000000000000000000000000 | |
| 497 | + | 6e6574207469746c650000001c00360101005000f30300000000000000000000 | |
| 498 | + | 0000000000000000000000000108370103000000ef0300000000000009000000 | |
| 499 | + | 0000000000000000000000006e6574207469746c650000001c00370101005000 | |
| 500 | + | f303000000000000000000000000000000000000000000000108380103000000 | |
| 501 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 502 | + | 650000001c00380101005000f303000000000000000000000000000000000000 | |
| 503 | + | 000000000108390103000000ef03000000000000090000000000000000000000 | |
| 504 | + | 000000006e6574207469746c650000001c00390101005000f303000000000000 | |
| 505 | + | 0000000000000000000000000000000001083a0103000000ef03000000000000 | |
| 506 | + | 090000000000000000000000000000006e6574207469746c650000001c003a01 | |
| 507 | + | 01005000f3030000000000000000000000000000000000000000000001083b01 | |
| 508 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 509 | + | 7469746c650000001c003b0101005000f3030000000000000000000000000000 | |
| 510 | + | 000000000000000001083c0103000000ef030000000000000900000000000000 | |
| 511 | + | 00000000000000006e6574207469746c650000001c003c0101005000f3030000 | |
| 512 | + | 000000000000000000000000000000000000000001083d0103000000ef030000 | |
| 513 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 514 | + | 1c003d0101005000f30300000000000000000000000000000000000000000000 | |
| 515 | + | 01083e0103000000ef0300000000000009000000000000000000000000000000 | |
| 516 | + | 6e6574207469746c650000001c003e0101005000f30300000000000000000000 | |
| 517 | + | 00000000000000000000000001083f0103000000ef0300000000000009000000 | |
| 518 | + | 0000000000000000000000006e6574207469746c650000001c003f0101005000 | |
| 519 | + | f303000000000000000000000000000000000000000000000108400103000000 | |
| 520 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 521 | + | 650000001c00400101005000f303000000000000000000000000000000000000 | |
| 522 | + | 000000000108410103000000ef03000000000000090000000000000000000000 | |
| 523 | + | 000000006e6574207469746c650000001c00410101005000f303000000000000 | |
| 524 | + | 000000000000000000000000000000000108420103000000ef03000000000000 | |
| 525 | + | 090000000000000000000000000000006e6574207469746c650000001c004201 | |
| 526 | + | 01005000f3030000000000000000000000000000000000000000000001084301 | |
| 527 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 528 | + | 7469746c650000001c00430101005000f3030000000000000000000000000000 | |
| 529 | + | 00000000000000000108440103000000ef030000000000000900000000000000 | |
| 530 | + | 00000000000000006e6574207469746c650000001c00440101005000f3030000 | |
| 531 | + | 00000000000000000000000000000000000000000108450103000000ef030000 | |
| 532 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 533 | + | 1c00450101005000f30300000000000000000000000000000000000000000000 | |
| 534 | + | 0108460103000000ef0300000000000009000000000000000000000000000000 | |
| 535 | + | 6e6574207469746c650000001c00460101005000f30300000000000000000000 | |
| 536 | + | 0000000000000000000000000108470103000000ef0300000000000009000000 | |
| 537 | + | 0000000000000000000000006e6574207469746c650000001c00470101005000 | |
| 538 | + | f303000000000000000000000000000000000000000000000108480103000000 | |
| 539 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 540 | + | 650000001c00480101005000f303000000000000000000000000000000000000 | |
| 541 | + | 000000000108490103000000ef03000000000000090000000000000000000000 | |
| 542 | + | 000000006e6574207469746c650000001c00490101005000f303000000000000 | |
| 543 | + | 0000000000000000000000000000000001084a0103000000ef03000000000000 | |
| 544 | + | 090000000000000000000000000000006e6574207469746c650000001c004a01 | |
| 545 | + | 01005000f3030000000000000000000000000000000000000000000001084b01 | |
| 546 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 547 | + | 7469746c650000001c004b0101005000f3030000000000000000000000000000 | |
| 548 | + | 000000000000000001084c0103000000ef030000000000000900000000000000 | |
| 549 | + | 00000000000000006e6574207469746c650000001c004c0101005000f3030000 | |
| 550 | + | 000000000000000000000000000000000000000001084d0103000000ef030000 | |
| 551 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 552 | + | 1c004d0101005000f30300000000000000000000000000000000000000000000 | |
| 553 | + | 01084e0103000000ef0300000000000009000000000000000000000000000000 | |
| 554 | + | 6e6574207469746c650000001c004e0101005000f30300000000000000000000 | |
| 555 | + | 00000000000000000000000001084f0103000000ef0300000000000009000000 | |
| 556 | + | 0000000000000000000000006e6574207469746c650000001c004f0101005000 | |
| 557 | + | f303000000000000000000000000000000000000000000000108500103000000 | |
| 558 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 559 | + | 650000001c00500101005000f303000000000000000000000000000000000000 | |
| 560 | + | 000000000108510103000000ef03000000000000090000000000000000000000 | |
| 561 | + | 000000006e6574207469746c650000001c00510101005000f303000000000000 | |
| 562 | + | 000000000000000000000000000000000108520103000000ef03000000000000 | |
| 563 | + | 090000000000000000000000000000006e6574207469746c650000001c005201 | |
| 564 | + | 01005000f3030000000000000000000000000000000000000000000001085301 | |
| 565 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 566 | + | 7469746c650000001c00530101005000f3030000000000000000000000000000 | |
| 567 | + | 00000000000000000108540103000000ef030000000000000900000000000000 | |
| 568 | + | 00000000000000006e6574207469746c650000001c00540101005000f3030000 | |
| 569 | + | 00000000000000000000000000000000000000000108550103000000ef030000 | |
| 570 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 571 | + | 1c00550101005000f30300000000000000000000000000000000000000000000 | |
| 572 | + | 0108560103000000ef0300000000000009000000000000000000000000000000 | |
| 573 | + | 6e6574207469746c650000001c00560101005000f30300000000000000000000 | |
| 574 | + | 0000000000000000000000000108570103000000ef0300000000000009000000 | |
| 575 | + | 0000000000000000000000006e6574207469746c650000001c00570101005000 | |
| 576 | + | f303000000000000000000000000000000000000000000000108580103000000 | |
| 577 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 578 | + | 650000001c00580101005000f303000000000000000000000000000000000000 | |
| 579 | + | 000000000108590103000000ef03000000000000090000000000000000000000 | |
| 580 | + | 000000006e6574207469746c650000001c00590101005000f303000000000000 | |
| 581 | + | 0000000000000000000000000000000001085a0103000000ef03000000000000 | |
| 582 | + | 090000000000000000000000000000006e6574207469746c650000001c005a01 | |
| 583 | + | 01005000f3030000000000000000000000000000000000000000000001085b01 | |
| 584 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 585 | + | 7469746c650000001c005b0101005000f3030000000000000000000000000000 | |
| 586 | + | 000000000000000001085c0103000000ef030000000000000900000000000000 | |
| 587 | + | 00000000000000006e6574207469746c650000001c005c0101005000f3030000 | |
| 588 | + | 000000000000000000000000000000000000000001085d0103000000ef030000 | |
| 589 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 590 | + | 1c005d0101005000f30300000000000000000000000000000000000000000000 | |
| 591 | + | 01085e0103000000ef0300000000000009000000000000000000000000000000 | |
| 592 | + | 6e6574207469746c650000001c005e0101005000f30300000000000000000000 | |
| 593 | + | 00000000000000000000000001085f0103000000ef0300000000000009000000 | |
| 594 | + | 0000000000000000000000006e6574207469746c650000001c005f0101005000 | |
| 595 | + | f303000000000000000000000000000000000000000000000108600103000000 | |
| 596 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 597 | + | 650000001c00600101005000f303000000000000000000000000000000000000 | |
| 598 | + | 000000000108610103000000ef03000000000000090000000000000000000000 | |
| 599 | + | 000000006e6574207469746c650000001c00610101005000f303000000000000 | |
| 600 | + | 000000000000000000000000000000000108620103000000ef03000000000000 | |
| 601 | + | 090000000000000000000000000000006e6574207469746c650000001c006201 | |
| 602 | + | 01005000f3030000000000000000000000000000000000000000000001086301 | |
| 603 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 604 | + | 7469746c650000001c00630101005000f3030000000000000000000000000000 | |
| 605 | + | 00000000000000000108640103000000ef030000000000000900000000000000 | |
| 606 | + | 00000000000000006e6574207469746c650000001c00640101005000f3030000 | |
| 607 | + | 00000000000000000000000000000000000000000108650103000000ef030000 | |
| 608 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 609 | + | 1c00650101005000f30300000000000000000000000000000000000000000000 | |
| 610 | + | 0108660103000000ef0300000000000009000000000000000000000000000000 | |
| 611 | + | 6e6574207469746c650000001c00660101005000f30300000000000000000000 | |
| 612 | + | 0000000000000000000000000108670103000000ef0300000000000009000000 | |
| 613 | + | 0000000000000000000000006e6574207469746c650000001c00670101005000 | |
| 614 | + | f303000000000000000000000000000000000000000000000108680103000000 | |
| 615 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 616 | + | 650000001c00680101005000f303000000000000000000000000000000000000 | |
| 617 | + | 000000000108690103000000ef03000000000000090000000000000000000000 | |
| 618 | + | 000000006e6574207469746c650000001c00690101005000f303000000000000 | |
| 619 | + | 0000000000000000000000000000000001086a0103000000ef03000000000000 | |
| 620 | + | 090000000000000000000000000000006e6574207469746c650000001c006a01 | |
| 621 | + | 01005000f3030000000000000000000000000000000000000000000001086b01 | |
| 622 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 623 | + | 7469746c650000001c006b0101005000f3030000000000000000000000000000 | |
| 624 | + | 000000000000000001086c0103000000ef030000000000000900000000000000 | |
| 625 | + | 00000000000000006e6574207469746c650000001c006c0101005000f3030000 | |
| 626 | + | 000000000000000000000000000000000000000001086d0103000000ef030000 | |
| 627 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 628 | + | 1c006d0101005000f30300000000000000000000000000000000000000000000 | |
| 629 | + | 01086e0103000000ef0300000000000009000000000000000000000000000000 | |
| 630 | + | 6e6574207469746c650000001c006e0101005000f30300000000000000000000 | |
| 631 | + | 00000000000000000000000001086f0103000000ef0300000000000009000000 | |
| 632 | + | 0000000000000000000000006e6574207469746c650000001c006f0101005000 | |
| 633 | + | f303000000000000000000000000000000000000000000000108700103000000 | |
| 634 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 635 | + | 650000001c00700101005000f303000000000000000000000000000000000000 | |
| 636 | + | 000000000108710103000000ef03000000000000090000000000000000000000 | |
| 637 | + | 000000006e6574207469746c650000001c00710101005000f303000000000000 | |
| 638 | + | 000000000000000000000000000000000108720103000000ef03000000000000 | |
| 639 | + | 090000000000000000000000000000006e6574207469746c650000001c007201 | |
| 640 | + | 01005000f3030000000000000000000000000000000000000000000001087301 | |
| 641 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 642 | + | 7469746c650000001c00730101005000f3030000000000000000000000000000 | |
| 643 | + | 00000000000000000108740103000000ef030000000000000900000000000000 | |
| 644 | + | 00000000000000006e6574207469746c650000001c00740101005000f3030000 | |
| 645 | + | 00000000000000000000000000000000000000000108750103000000ef030000 | |
| 646 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 647 | + | 1c00750101005000f30300000000000000000000000000000000000000000000 | |
| 648 | + | 0108760103000000ef0300000000000009000000000000000000000000000000 | |
| 649 | + | 6e6574207469746c650000001c00760101005000f30300000000000000000000 | |
| 650 | + | 0000000000000000000000000108770103000000ef0300000000000009000000 | |
| 651 | + | 0000000000000000000000006e6574207469746c650000001c00770101005000 | |
| 652 | + | f303000000000000000000000000000000000000000000000108780103000000 | |
| 653 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 654 | + | 650000001c00780101005000f303000000000000000000000000000000000000 | |
| 655 | + | 000000000108790103000000ef03000000000000090000000000000000000000 | |
| 656 | + | 000000006e6574207469746c650000001c00790101005000f303000000000000 | |
| 657 | + | 0000000000000000000000000000000001087a0103000000ef03000000000000 | |
| 658 | + | 090000000000000000000000000000006e6574207469746c650000001c007a01 | |
| 659 | + | 01005000f3030000000000000000000000000000000000000000000001087b01 | |
| 660 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 661 | + | 7469746c650000001c007b0101005000f3030000000000000000000000000000 | |
| 662 | + | 000000000000000001087c0103000000ef030000000000000900000000000000 | |
| 663 | + | 00000000000000006e6574207469746c650000001c007c0101005000f3030000 | |
| 664 | + | 000000000000000000000000000000000000000001087d0103000000ef030000 | |
| 665 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 666 | + | 1c007d0101005000f30300000000000000000000000000000000000000000000 | |
| 667 | + | 01087e0103000000ef0300000000000009000000000000000000000000000000 | |
| 668 | + | 6e6574207469746c650000001c007e0101005000f30300000000000000000000 | |
| 669 | + | 00000000000000000000000001087f0103000000ef0300000000000009000000 | |
| 670 | + | 0000000000000000000000006e6574207469746c650000001c007f0101005000 | |
| 671 | + | f303000000000000000000000000000000000000000000000108800103000000 | |
| 672 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 673 | + | 650000001c00800101005000f303000000000000000000000000000000000000 | |
| 674 | + | 000000000108810103000000ef03000000000000090000000000000000000000 | |
| 675 | + | 000000006e6574207469746c650000001c00810101005000f303000000000000 | |
| 676 | + | 000000000000000000000000000000000108820103000000ef03000000000000 | |
| 677 | + | 090000000000000000000000000000006e6574207469746c650000001c008201 | |
| 678 | + | 01005000f3030000000000000000000000000000000000000000000001088301 | |
| 679 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 680 | + | 7469746c650000001c00830101005000f3030000000000000000000000000000 | |
| 681 | + | 00000000000000000108840103000000ef030000000000000900000000000000 | |
| 682 | + | 00000000000000006e6574207469746c650000001c00840101005000f3030000 | |
| 683 | + | 00000000000000000000000000000000000000000108850103000000ef030000 | |
| 684 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 685 | + | 1c00850101005000f30300000000000000000000000000000000000000000000 | |
| 686 | + | 0108860103000000ef0300000000000009000000000000000000000000000000 | |
| 687 | + | 6e6574207469746c650000001c00860101005000f30300000000000000000000 | |
| 688 | + | 0000000000000000000000000108870103000000ef0300000000000009000000 | |
| 689 | + | 0000000000000000000000006e6574207469746c650000001c00870101005000 | |
| 690 | + | f303000000000000000000000000000000000000000000000108880103000000 | |
| 691 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 692 | + | 650000001c00880101005000f303000000000000000000000000000000000000 | |
| 693 | + | 000000000108890103000000ef03000000000000090000000000000000000000 | |
| 694 | + | 000000006e6574207469746c650000001c00890101005000f303000000000000 | |
| 695 | + | 0000000000000000000000000000000001088a0103000000ef03000000000000 | |
| 696 | + | 090000000000000000000000000000006e6574207469746c650000001c008a01 | |
| 697 | + | 01005000f3030000000000000000000000000000000000000000000001088b01 | |
| 698 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 699 | + | 7469746c650000001c008b0101005000f3030000000000000000000000000000 | |
| 700 | + | 000000000000000001088c0103000000ef030000000000000900000000000000 | |
| 701 | + | 00000000000000006e6574207469746c650000001c008c0101005000f3030000 | |
| 702 | + | 000000000000000000000000000000000000000001088d0103000000ef030000 | |
| 703 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 704 | + | 1c008d0101005000f30300000000000000000000000000000000000000000000 | |
| 705 | + | 01088e0103000000ef0300000000000009000000000000000000000000000000 | |
| 706 | + | 6e6574207469746c650000001c008e0101005000f30300000000000000000000 | |
| 707 | + | 00000000000000000000000001088f0103000000ef0300000000000009000000 | |
| 708 | + | 0000000000000000000000006e6574207469746c650000001c008f0101005000 | |
| 709 | + | f303000000000000000000000000000000000000000000000108900103000000 | |
| 710 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 711 | + | 650000001c00900101005000f303000000000000000000000000000000000000 | |
| 712 | + | 000000000108910103000000ef03000000000000090000000000000000000000 | |
| 713 | + | 000000006e6574207469746c650000001c00910101005000f303000000000000 | |
| 714 | + | 000000000000000000000000000000000108920103000000ef03000000000000 | |
| 715 | + | 090000000000000000000000000000006e6574207469746c650000001c009201 | |
| 716 | + | 01005000f3030000000000000000000000000000000000000000000001089301 | |
| 717 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 718 | + | 7469746c650000001c00930101005000f3030000000000000000000000000000 | |
| 719 | + | 00000000000000000108940103000000ef030000000000000900000000000000 | |
| 720 | + | 00000000000000006e6574207469746c650000001c00940101005000f3030000 | |
| 721 | + | 00000000000000000000000000000000000000000108950103000000ef030000 | |
| 722 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 723 | + | 1c00950101005000f30300000000000000000000000000000000000000000000 | |
| 724 | + | 0108960103000000ef0300000000000009000000000000000000000000000000 | |
| 725 | + | 6e6574207469746c650000001c00960101005000f30300000000000000000000 | |
| 726 | + | 0000000000000000000000000108970103000000ef0300000000000009000000 | |
| 727 | + | 0000000000000000000000006e6574207469746c650000001c00970101005000 | |
| 728 | + | f303000000000000000000000000000000000000000000000108980103000000 | |
| 729 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 730 | + | 650000001c00980101005000f303000000000000000000000000000000000000 | |
| 731 | + | 000000000108990103000000ef03000000000000090000000000000000000000 | |
| 732 | + | 000000006e6574207469746c650000001c00990101005000f303000000000000 | |
| 733 | + | 0000000000000000000000000000000001089a0103000000ef03000000000000 | |
| 734 | + | 090000000000000000000000000000006e6574207469746c650000001c009a01 | |
| 735 | + | 01005000f3030000000000000000000000000000000000000000000001089b01 | |
| 736 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 737 | + | 7469746c650000001c009b0101005000f3030000000000000000000000000000 | |
| 738 | + | 000000000000000001089c0103000000ef030000000000000900000000000000 | |
| 739 | + | 00000000000000006e6574207469746c650000001c009c0101005000f3030000 | |
| 740 | + | 000000000000000000000000000000000000000001089d0103000000ef030000 | |
| 741 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 742 | + | 1c009d0101005000f30300000000000000000000000000000000000000000000 | |
| 743 | + | 01089e0103000000ef0300000000000009000000000000000000000000000000 | |
| 744 | + | 6e6574207469746c650000001c009e0101005000f30300000000000000000000 | |
| 745 | + | 00000000000000000000000001089f0103000000ef0300000000000009000000 | |
| 746 | + | 0000000000000000000000006e6574207469746c650000001c009f0101005000 | |
| 747 | + | f303000000000000000000000000000000000000000000000108a00103000000 | |
| 748 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 749 | + | 650000001c00a00101005000f303000000000000000000000000000000000000 | |
| 750 | + | 000000000108a10103000000ef03000000000000090000000000000000000000 | |
| 751 | + | 000000006e6574207469746c650000001c00a10101005000f303000000000000 | |
| 752 | + | 000000000000000000000000000000000108a20103000000ef03000000000000 | |
| 753 | + | 090000000000000000000000000000006e6574207469746c650000001c00a201 | |
| 754 | + | 01005000f303000000000000000000000000000000000000000000000108a301 | |
| 755 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 756 | + | 7469746c650000001c00a30101005000f3030000000000000000000000000000 | |
| 757 | + | 00000000000000000108a40103000000ef030000000000000900000000000000 | |
| 758 | + | 00000000000000006e6574207469746c650000001c00a40101005000f3030000 | |
| 759 | + | 00000000000000000000000000000000000000000108a50103000000ef030000 | |
| 760 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 761 | + | 1c00a50101005000f30300000000000000000000000000000000000000000000 | |
| 762 | + | 0108a60103000000ef0300000000000009000000000000000000000000000000 | |
| 763 | + | 6e6574207469746c650000001c00a60101005000f30300000000000000000000 | |
| 764 | + | 0000000000000000000000000108a70103000000ef0300000000000009000000 | |
| 765 | + | 0000000000000000000000006e6574207469746c650000001c00a70101005000 | |
| 766 | + | f303000000000000000000000000000000000000000000000108a80103000000 | |
| 767 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 768 | + | 650000001c00a80101005000f303000000000000000000000000000000000000 | |
| 769 | + | 000000000108a90103000000ef03000000000000090000000000000000000000 | |
| 770 | + | 000000006e6574207469746c650000001c00a90101005000f303000000000000 | |
| 771 | + | 000000000000000000000000000000000108aa0103000000ef03000000000000 | |
| 772 | + | 090000000000000000000000000000006e6574207469746c650000001c00aa01 | |
| 773 | + | 01005000f303000000000000000000000000000000000000000000000108ab01 | |
| 774 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 775 | + | 7469746c650000001c00ab0101005000f3030000000000000000000000000000 | |
| 776 | + | 00000000000000000108ac0103000000ef030000000000000900000000000000 | |
| 777 | + | 00000000000000006e6574207469746c650000001c00ac0101005000f3030000 | |
| 778 | + | 00000000000000000000000000000000000000000108ad0103000000ef030000 | |
| 779 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 780 | + | 1c00ad0101005000f30300000000000000000000000000000000000000000000 | |
| 781 | + | 0108ae0103000000ef0300000000000009000000000000000000000000000000 | |
| 782 | + | 6e6574207469746c650000001c00ae0101005000f30300000000000000000000 | |
| 783 | + | 0000000000000000000000000108af0103000000ef0300000000000009000000 | |
| 784 | + | 0000000000000000000000006e6574207469746c650000001c00af0101005000 | |
| 785 | + | f303000000000000000000000000000000000000000000000108b00103000000 | |
| 786 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 787 | + | 650000001c00b00101005000f303000000000000000000000000000000000000 | |
| 788 | + | 000000000108b10103000000ef03000000000000090000000000000000000000 | |
| 789 | + | 000000006e6574207469746c650000001c00b10101005000f303000000000000 | |
| 790 | + | 000000000000000000000000000000000108b20103000000ef03000000000000 | |
| 791 | + | 090000000000000000000000000000006e6574207469746c650000001c00b201 | |
| 792 | + | 01005000f303000000000000000000000000000000000000000000000108b301 | |
| 793 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 794 | + | 7469746c650000001c00b30101005000f3030000000000000000000000000000 | |
| 795 | + | 00000000000000000108b40103000000ef030000000000000900000000000000 | |
| 796 | + | 00000000000000006e6574207469746c650000001c00b40101005000f3030000 | |
| 797 | + | 00000000000000000000000000000000000000000108b50103000000ef030000 | |
| 798 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 799 | + | 1c00b50101005000f30300000000000000000000000000000000000000000000 | |
| 800 | + | 0108b60103000000ef0300000000000009000000000000000000000000000000 | |
| 801 | + | 6e6574207469746c650000001c00b60101005000f30300000000000000000000 | |
| 802 | + | 0000000000000000000000000108b70103000000ef0300000000000009000000 | |
| 803 | + | 0000000000000000000000006e6574207469746c650000001c00b70101005000 | |
| 804 | + | f303000000000000000000000000000000000000000000000108b80103000000 | |
| 805 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 806 | + | 650000001c00b80101005000f303000000000000000000000000000000000000 | |
| 807 | + | 000000000108b90103000000ef03000000000000090000000000000000000000 | |
| 808 | + | 000000006e6574207469746c650000001c00b90101005000f303000000000000 | |
| 809 | + | 000000000000000000000000000000000108ba0103000000ef03000000000000 | |
| 810 | + | 090000000000000000000000000000006e6574207469746c650000001c00ba01 | |
| 811 | + | 01005000f303000000000000000000000000000000000000000000000108bb01 | |
| 812 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 813 | + | 7469746c650000001c00bb0101005000f3030000000000000000000000000000 | |
| 814 | + | 00000000000000000108bc0103000000ef030000000000000900000000000000 | |
| 815 | + | 00000000000000006e6574207469746c650000001c00bc0101005000f3030000 | |
| 816 | + | 00000000000000000000000000000000000000000108bd0103000000ef030000 | |
| 817 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 818 | + | 1c00bd0101005000f30300000000000000000000000000000000000000000000 | |
| 819 | + | 0108be0103000000ef0300000000000009000000000000000000000000000000 | |
| 820 | + | 6e6574207469746c650000001c00be0101005000f30300000000000000000000 | |
| 821 | + | 0000000000000000000000000108bf0103000000ef0300000000000009000000 | |
| 822 | + | 0000000000000000000000006e6574207469746c650000001c00bf0101005000 | |
| 823 | + | f303000000000000000000000000000000000000000000000108c00103000000 | |
| 824 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 825 | + | 650000001c00c00101005000f303000000000000000000000000000000000000 | |
| 826 | + | 000000000108c10103000000ef03000000000000090000000000000000000000 | |
| 827 | + | 000000006e6574207469746c650000001c00c10101005000f303000000000000 | |
| 828 | + | 000000000000000000000000000000000108c20103000000ef03000000000000 | |
| 829 | + | 090000000000000000000000000000006e6574207469746c650000001c00c201 | |
| 830 | + | 01005000f303000000000000000000000000000000000000000000000108c301 | |
| 831 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 832 | + | 7469746c650000001c00c30101005000f3030000000000000000000000000000 | |
| 833 | + | 00000000000000000108c40103000000ef030000000000000900000000000000 | |
| 834 | + | 00000000000000006e6574207469746c650000001c00c40101005000f3030000 | |
| 835 | + | 00000000000000000000000000000000000000000108c50103000000ef030000 | |
| 836 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 837 | + | 1c00c50101005000f30300000000000000000000000000000000000000000000 | |
| 838 | + | 0108c60103000000ef0300000000000009000000000000000000000000000000 | |
| 839 | + | 6e6574207469746c650000001c00c60101005000f30300000000000000000000 | |
| 840 | + | 0000000000000000000000000108c70103000000ef0300000000000009000000 | |
| 841 | + | 0000000000000000000000006e6574207469746c650000001c00c70101005000 | |
| 842 | + | f303000000000000000000000000000000000000000000000108c80103000000 | |
| 843 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 844 | + | 650000001c00c80101005000f303000000000000000000000000000000000000 | |
| 845 | + | 000000000108c90103000000ef03000000000000090000000000000000000000 | |
| 846 | + | 000000006e6574207469746c650000001c00c90101005000f303000000000000 | |
| 847 | + | 000000000000000000000000000000000108ca0103000000ef03000000000000 | |
| 848 | + | 090000000000000000000000000000006e6574207469746c650000001c00ca01 | |
| 849 | + | 01005000f303000000000000000000000000000000000000000000000108cb01 | |
| 850 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 851 | + | 7469746c650000001c00cb0101005000f3030000000000000000000000000000 | |
| 852 | + | 00000000000000000108cc0103000000ef030000000000000900000000000000 | |
| 853 | + | 00000000000000006e6574207469746c650000001c00cc0101005000f3030000 | |
| 854 | + | 00000000000000000000000000000000000000000108cd0103000000ef030000 | |
| 855 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 856 | + | 1c00cd0101005000f30300000000000000000000000000000000000000000000 | |
| 857 | + | 0108ce0103000000ef0300000000000009000000000000000000000000000000 | |
| 858 | + | 6e6574207469746c650000001c00ce0101005000f30300000000000000000000 | |
| 859 | + | 0000000000000000000000000108cf0103000000ef0300000000000009000000 | |
| 860 | + | 0000000000000000000000006e6574207469746c650000001c00cf0101005000 | |
| 861 | + | f303000000000000000000000000000000000000000000000108d00103000000 | |
| 862 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 863 | + | 650000001c00d00101005000f303000000000000000000000000000000000000 | |
| 864 | + | 000000000108d10103000000ef03000000000000090000000000000000000000 | |
| 865 | + | 000000006e6574207469746c650000001c00d10101005000f303000000000000 | |
| 866 | + | 000000000000000000000000000000000108d20103000000ef03000000000000 | |
| 867 | + | 090000000000000000000000000000006e6574207469746c650000001c00d201 | |
| 868 | + | 01005000f303000000000000000000000000000000000000000000000108d301 | |
| 869 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 870 | + | 7469746c650000001c00d30101005000f3030000000000000000000000000000 | |
| 871 | + | 00000000000000000108d40103000000ef030000000000000900000000000000 | |
| 872 | + | 00000000000000006e6574207469746c650000001c00d40101005000f3030000 | |
| 873 | + | 00000000000000000000000000000000000000000108d50103000000ef030000 | |
| 874 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 875 | + | 1c00d50101005000f30300000000000000000000000000000000000000000000 | |
| 876 | + | 0108d60103000000ef0300000000000009000000000000000000000000000000 | |
| 877 | + | 6e6574207469746c650000001c00d60101005000f30300000000000000000000 | |
| 878 | + | 0000000000000000000000000108d70103000000ef0300000000000009000000 | |
| 879 | + | 0000000000000000000000006e6574207469746c650000001c00d70101005000 | |
| 880 | + | f303000000000000000000000000000000000000000000000108d80103000000 | |
| 881 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 882 | + | 650000001c00d80101005000f303000000000000000000000000000000000000 | |
| 883 | + | 000000000108d90103000000ef03000000000000090000000000000000000000 | |
| 884 | + | 000000006e6574207469746c650000001c00d90101005000f303000000000000 | |
| 885 | + | 000000000000000000000000000000000108da0103000000ef03000000000000 | |
| 886 | + | 090000000000000000000000000000006e6574207469746c650000001c00da01 | |
| 887 | + | 01005000f303000000000000000000000000000000000000000000000108db01 | |
| 888 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 889 | + | 7469746c650000001c00db0101005000f3030000000000000000000000000000 | |
| 890 | + | 00000000000000000108dc0103000000ef030000000000000900000000000000 | |
| 891 | + | 00000000000000006e6574207469746c650000001c00dc0101005000f3030000 | |
| 892 | + | 00000000000000000000000000000000000000000108dd0103000000ef030000 | |
| 893 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 894 | + | 1c00dd0101005000f30300000000000000000000000000000000000000000000 | |
| 895 | + | 0108de0103000000ef0300000000000009000000000000000000000000000000 | |
| 896 | + | 6e6574207469746c650000001c00de0101005000f30300000000000000000000 | |
| 897 | + | 0000000000000000000000000108df0103000000ef0300000000000009000000 | |
| 898 | + | 0000000000000000000000006e6574207469746c650000001c00df0101005000 | |
| 899 | + | f303000000000000000000000000000000000000000000000108e00103000000 | |
| 900 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 901 | + | 650000001c00e00101005000f303000000000000000000000000000000000000 | |
| 902 | + | 000000000108e10103000000ef03000000000000090000000000000000000000 | |
| 903 | + | 000000006e6574207469746c650000001c00e10101005000f303000000000000 | |
| 904 | + | 000000000000000000000000000000000108e20103000000ef03000000000000 | |
| 905 | + | 090000000000000000000000000000006e6574207469746c650000001c00e201 | |
| 906 | + | 01005000f303000000000000000000000000000000000000000000000108e301 | |
| 907 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 908 | + | 7469746c650000001c00e30101005000f3030000000000000000000000000000 | |
| 909 | + | 00000000000000000108e40103000000ef030000000000000900000000000000 | |
| 910 | + | 00000000000000006e6574207469746c650000001c00e40101005000f3030000 | |
| 911 | + | 00000000000000000000000000000000000000000108e50103000000ef030000 | |
| 912 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 913 | + | 1c00e50101005000f30300000000000000000000000000000000000000000000 | |
| 914 | + | 0108e60103000000ef0300000000000009000000000000000000000000000000 | |
| 915 | + | 6e6574207469746c650000001c00e60101005000f30300000000000000000000 | |
| 916 | + | 0000000000000000000000000108e70103000000ef0300000000000009000000 | |
| 917 | + | 0000000000000000000000006e6574207469746c650000001c00e70101005000 | |
| 918 | + | f303000000000000000000000000000000000000000000000108e80103000000 | |
| 919 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 920 | + | 650000001c00e80101005000f303000000000000000000000000000000000000 | |
| 921 | + | 000000000108e90103000000ef03000000000000090000000000000000000000 | |
| 922 | + | 000000006e6574207469746c650000001c00e90101005000f303000000000000 | |
| 923 | + | 000000000000000000000000000000000108ea0103000000ef03000000000000 | |
| 924 | + | 090000000000000000000000000000006e6574207469746c650000001c00ea01 | |
| 925 | + | 01005000f303000000000000000000000000000000000000000000000108eb01 | |
| 926 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 927 | + | 7469746c650000001c00eb0101005000f3030000000000000000000000000000 | |
| 928 | + | 00000000000000000108ec0103000000ef030000000000000900000000000000 | |
| 929 | + | 00000000000000006e6574207469746c650000001c00ec0101005000f3030000 | |
| 930 | + | 00000000000000000000000000000000000000000108ed0103000000ef030000 | |
| 931 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
| 932 | + | 1c00ed0101005000f30300000000000000000000000000000000000000000000 | |
| 933 | + | 0108ee0103000000ef0300000000000009000000000000000000000000000000 | |
| 934 | + | 6e6574207469746c650000001c00ee0101005000f30300000000000000000000 | |
| 935 | + | 0000000000000000000000000108ef0103000000ef0300000000000009000000 | |
| 936 | + | 0000000000000000000000006e6574207469746c650000001c00ef0101005000 | |
| 937 | + | f303000000000000000000000000000000000000000000000108f00103000000 | |
| 938 | + | ef03000000000000090000000000000000000000000000006e6574207469746c | |
| 939 | + | 650000001c00f00101005000f303000000000000000000000000000000000000 | |
| 940 | + | 000000000108f10103000000ef03000000000000090000000000000000000000 | |
| 941 | + | 000000006e6574207469746c650000001c00f10101005000f303000000000000 | |
| 942 | + | 000000000000000000000000000000000108f20103000000ef03000000000000 | |
| 943 | + | 090000000000000000000000000000006e6574207469746c650000001c00f201 | |
| 944 | + | 01005000f303000000000000000000000000000000000000000000000108f301 | |
| 945 | + | 03000000ef03000000000000090000000000000000000000000000006e657420 | |
| 946 | + | 7469746c650000001c00f30101005000f3030000000000000000000000000000 | |
| 947 | + | 00000000000000000108f40103000000ef030000000000000900000000000000 | |
| 948 | + | 00000000000000006e6574207469746c650000001c00f40101005000f3030000 | |
| 949 | + | 00000000000000000000000000000000000000000108f50103000000ef030000 | |
| 950 | + | 00000000090000000000000000000000000000006e6574207469746c65000000 | |
A internal/adapters/wayland/xwm/testdata/streams/steady.hex +2150 -0 large diff (2150 lines changed): collapsed
A internal/adapters/wayland/xwm/xpeer_test.go +594 -0
| @@ -0,0 +1,594 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "io" | |
| 6 | + | "os" | |
| 7 | + | "sync" | |
| 8 | + | "sync/atomic" | |
| 9 | + | "testing" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "golang.org/x/sys/unix" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // Opcodes the peer understands. | |
| 16 | + | const ( | |
| 17 | + | opCreateWindow = 1 | |
| 18 | + | opChangeWindowAttributes = 2 | |
| 19 | + | opGetWindowAttributes = 3 | |
| 20 | + | opMapWindow = 8 | |
| 21 | + | opDeleteProperty = 19 | |
| 22 | + | opConfigureWindow = 12 | |
| 23 | + | opGetGeometry = 14 | |
| 24 | + | opQueryTree = 15 | |
| 25 | + | opInternAtom = 16 | |
| 26 | + | opChangeProperty = 18 | |
| 27 | + | opGetProperty = 20 | |
| 28 | + | opSetSelectionOwner = 22 | |
| 29 | + | opSendEvent = 25 | |
| 30 | + | opSetInputFocus = 42 | |
| 31 | + | opGetInputFocus = 43 | |
| 32 | + | opQueryExtension = 98 | |
| 33 | + | opKillClient = 113 | |
| 34 | + | ) | |
| 35 | + | ||
| 36 | + | const ( | |
| 37 | + | peerRoot = 0x100 | |
| 38 | + | peerXIDBase = 0x200000 | |
| 39 | + | peerXIDMask = 0x1fffff | |
| 40 | + | peerMaxWords = 0x100000 | |
| 41 | + | ) | |
| 42 | + | ||
| 43 | + | type xpeerExt struct{ major, firstEvent, firstError uint8 } | |
| 44 | + | ||
| 45 | + | // xpeerReq is one request the peer received. | |
| 46 | + | type xpeerReq struct { | |
| 47 | + | seq uint16 | |
| 48 | + | major uint8 | |
| 49 | + | minor uint8 | |
| 50 | + | raw []byte // whole request, header included | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | func (r xpeerReq) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) } | |
| 54 | + | ||
| 55 | + | type xpeerProp struct { | |
| 56 | + | typ uint32 | |
| 57 | + | format uint8 | |
| 58 | + | data []byte | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | type xpeerGeom struct { | |
| 62 | + | x, y int16 | |
| 63 | + | w, h, border uint16 | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // xpeer is a scripted X server speaking the real wire protocol over a | |
| 67 | + | // socketpair: setup, extension queries, atoms, properties, geometry, window | |
| 68 | + | // tree. It records every request. It is a protocol peer, not a test double of | |
| 69 | + | // a project interface. | |
| 70 | + | type xpeer struct { | |
| 71 | + | t *testing.T | |
| 72 | + | f *os.File | |
| 73 | + | ||
| 74 | + | mu sync.Mutex | |
| 75 | + | seq uint16 | |
| 76 | + | reqs []xpeerReq | |
| 77 | + | exts map[string]xpeerExt | |
| 78 | + | atoms map[string]uint32 | |
| 79 | + | nextAtom uint32 | |
| 80 | + | props map[uint32]map[uint32]xpeerProp | |
| 81 | + | geoms map[uint32]xpeerGeom | |
| 82 | + | attrs map[uint32][2]byte // window -> {overrideRedirect, mapState} | |
| 83 | + | children []uint32 | |
| 84 | + | denyRoot bool // answer the root event-mask request with BadAccess | |
| 85 | + | pids map[uint32]uint32 // XRes QueryClientIds answers, by window | |
| 86 | + | bad map[uint32]bool // windows whose GetProperty/GetGeometry fail with BadWindow | |
| 87 | + | hold bool // keep replies until release | |
| 88 | + | held [][]byte | |
| 89 | + | tap []byte // everything written to the client, when recording | |
| 90 | + | record bool | |
| 91 | + | ||
| 92 | + | gate atomic.Pointer[chan struct{}] // closed = reading allowed | |
| 93 | + | sbuf [4096]byte | |
| 94 | + | sink atomic.Bool // discard requests without recording or allocating | |
| 95 | + | done chan struct{} | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func fullPeerExts() map[string]xpeerExt { | |
| 99 | + | return map[string]xpeerExt{ | |
| 100 | + | "BIG-REQUESTS": {133, 0, 0}, | |
| 101 | + | "XC-MISC": {136, 0, 0}, | |
| 102 | + | "Composite": {142, 0, 0}, | |
| 103 | + | "XFIXES": {138, 87, 140}, | |
| 104 | + | "X-Resource": {144, 0, 0}, | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // startXPeer returns the client end of a socketpair, to give to Boot, and the | |
| 109 | + | // peer serving the other end. | |
| 110 | + | func startXPeer(t *testing.T, exts map[string]xpeerExt) (int, *xpeer) { | |
| 111 | + | t.Helper() | |
| 112 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 113 | + | if err != nil { | |
| 114 | + | t.Fatal(err) | |
| 115 | + | } | |
| 116 | + | p := &xpeer{ | |
| 117 | + | t: t, f: os.NewFile(uintptr(fds[1]), "xpeer"), | |
| 118 | + | exts: exts, atoms: map[string]uint32{}, nextAtom: 1000, | |
| 119 | + | props: map[uint32]map[uint32]xpeerProp{}, geoms: map[uint32]xpeerGeom{}, attrs: map[uint32][2]byte{}, | |
| 120 | + | pids: map[uint32]uint32{}, bad: map[uint32]bool{}, | |
| 121 | + | done: make(chan struct{}), | |
| 122 | + | } | |
| 123 | + | open := make(chan struct{}) | |
| 124 | + | close(open) | |
| 125 | + | p.gate.Store(&open) | |
| 126 | + | t.Cleanup(func() { | |
| 127 | + | p.resume() // a paused peer would never see the close | |
| 128 | + | p.f.Close() | |
| 129 | + | <-p.done | |
| 130 | + | }) | |
| 131 | + | go p.serve() | |
| 132 | + | return fds[0], p | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func (p *xpeer) setupReply() []byte { | |
| 136 | + | vendor := []byte("xpeer\x00\x00\x00") | |
| 137 | + | b := make([]byte, 40+len(vendor)+40) | |
| 138 | + | b[0], b[2] = 1, 11 | |
| 139 | + | binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4)) | |
| 140 | + | binary.LittleEndian.PutUint32(b[12:], peerXIDBase) | |
| 141 | + | binary.LittleEndian.PutUint32(b[16:], peerXIDMask) | |
| 142 | + | binary.LittleEndian.PutUint16(b[24:], 5) | |
| 143 | + | b[28] = 1 | |
| 144 | + | copy(b[40:], vendor) | |
| 145 | + | scr := b[40+len(vendor):] | |
| 146 | + | binary.LittleEndian.PutUint32(scr, peerRoot) | |
| 147 | + | binary.LittleEndian.PutUint32(scr[32:], 0x22) // root visual | |
| 148 | + | scr[38] = 24 // root depth | |
| 149 | + | return b | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | func (p *xpeer) serve() { | |
| 153 | + | defer close(p.done) | |
| 154 | + | var hdr [12]byte | |
| 155 | + | if _, err := io.ReadFull(p.f, hdr[:]); err != nil { | |
| 156 | + | return | |
| 157 | + | } | |
| 158 | + | // Auth name and data are empty (NewConn sends none). | |
| 159 | + | if _, err := p.f.Write(p.setupReply()); err != nil { | |
| 160 | + | return | |
| 161 | + | } | |
| 162 | + | for { | |
| 163 | + | <-*p.gate.Load() | |
| 164 | + | if p.sink.Load() { | |
| 165 | + | if !p.discard() { | |
| 166 | + | return | |
| 167 | + | } | |
| 168 | + | continue | |
| 169 | + | } | |
| 170 | + | var h [4]byte | |
| 171 | + | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |
| 172 | + | return | |
| 173 | + | } | |
| 174 | + | raw := h[:] | |
| 175 | + | n := int(binary.LittleEndian.Uint16(h[2:])) * 4 | |
| 176 | + | if n == 0 { // BIG-REQUESTS header | |
| 177 | + | var l [4]byte | |
| 178 | + | if _, err := io.ReadFull(p.f, l[:]); err != nil { | |
| 179 | + | return | |
| 180 | + | } | |
| 181 | + | raw = append(raw, l[:]...) | |
| 182 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |
| 183 | + | } | |
| 184 | + | rest := make([]byte, n-len(raw)) | |
| 185 | + | if _, err := io.ReadFull(p.f, rest); err != nil { | |
| 186 | + | return | |
| 187 | + | } | |
| 188 | + | raw = append(append([]byte(nil), raw...), rest...) | |
| 189 | + | p.mu.Lock() | |
| 190 | + | p.seq++ | |
| 191 | + | r := xpeerReq{p.seq, h[0], h[1], raw} | |
| 192 | + | p.reqs = append(p.reqs, r) | |
| 193 | + | p.answer(r, raw[len(raw)-len(rest):]) | |
| 194 | + | p.mu.Unlock() | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | // discard reads and drops one request without allocating. It reports false | |
| 199 | + | // when the connection ended. | |
| 200 | + | func (p *xpeer) discard() bool { | |
| 201 | + | if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil { | |
| 202 | + | return false | |
| 203 | + | } | |
| 204 | + | n := int(binary.LittleEndian.Uint16(p.sbuf[2:])) * 4 | |
| 205 | + | left := n - 4 | |
| 206 | + | if n == 0 { | |
| 207 | + | if _, err := io.ReadFull(p.f, p.sbuf[:4]); err != nil { | |
| 208 | + | return false | |
| 209 | + | } | |
| 210 | + | left = int(binary.LittleEndian.Uint32(p.sbuf[:4]))*4 - 8 | |
| 211 | + | } | |
| 212 | + | for left > 0 { | |
| 213 | + | m, err := p.f.Read(p.sbuf[:min(left, len(p.sbuf))]) | |
| 214 | + | if err != nil { | |
| 215 | + | return false | |
| 216 | + | } | |
| 217 | + | left -= m | |
| 218 | + | } | |
| 219 | + | return true | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // write sends bytes to the client. The caller holds p.mu. | |
| 223 | + | func (p *xpeer) write(b []byte) { | |
| 224 | + | if p.record { | |
| 225 | + | p.tap = append(p.tap, b...) | |
| 226 | + | } | |
| 227 | + | _, _ = p.f.Write(b) | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | func (p *xpeer) reply(data uint8, body []byte) { | |
| 231 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |
| 232 | + | b := make([]byte, 32+4*words) | |
| 233 | + | b[0], b[1] = 1, data | |
| 234 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |
| 235 | + | binary.LittleEndian.PutUint32(b[4:], uint32(words)) | |
| 236 | + | copy(b[8:], body) | |
| 237 | + | if p.hold { | |
| 238 | + | p.held = append(p.held, b) | |
| 239 | + | return | |
| 240 | + | } | |
| 241 | + | p.write(b) | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | // release sends the held replies, in request order as a real server does. | |
| 245 | + | func (p *xpeer) release() { | |
| 246 | + | p.mu.Lock() | |
| 247 | + | defer p.mu.Unlock() | |
| 248 | + | p.hold = false | |
| 249 | + | for _, b := range p.held { | |
| 250 | + | p.write(b) | |
| 251 | + | } | |
| 252 | + | p.held = nil | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | // holdReplies keeps the replies of the following requests back. | |
| 256 | + | func (p *xpeer) holdReplies() { | |
| 257 | + | p.mu.Lock() | |
| 258 | + | p.hold = true | |
| 259 | + | p.mu.Unlock() | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | func (p *xpeer) errorPacket(code, major uint8, bad uint32) { | |
| 263 | + | b := make([]byte, 32) | |
| 264 | + | b[1] = code | |
| 265 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |
| 266 | + | binary.LittleEndian.PutUint32(b[4:], bad) | |
| 267 | + | b[10] = major | |
| 268 | + | p.write(b) | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | func (p *xpeer) extName(major uint8) string { | |
| 272 | + | for name, e := range p.exts { | |
| 273 | + | if e.major == major { | |
| 274 | + | return name | |
| 275 | + | } | |
| 276 | + | } | |
| 277 | + | return "" | |
| 278 | + | } | |
| 279 | + | ||
| 280 | + | func le32(v ...uint32) []byte { | |
| 281 | + | b := make([]byte, 0, 4*len(v)) | |
| 282 | + | for _, x := range v { | |
| 283 | + | b = binary.LittleEndian.AppendUint32(b, x) | |
| 284 | + | } | |
| 285 | + | return b | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | // answer runs with p.mu held. | |
| 289 | + | func (p *xpeer) answer(r xpeerReq, body []byte) { | |
| 290 | + | switch { | |
| 291 | + | case r.major == opQueryExtension: | |
| 292 | + | e, ok := p.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |
| 293 | + | var present uint8 | |
| 294 | + | if ok { | |
| 295 | + | present = 1 | |
| 296 | + | } | |
| 297 | + | p.reply(0, []byte{present, e.major, e.firstEvent, e.firstError}) | |
| 298 | + | case r.major == opInternAtom: | |
| 299 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |
| 300 | + | a, ok := p.atoms[name] | |
| 301 | + | if !ok { | |
| 302 | + | p.nextAtom++ | |
| 303 | + | a = p.nextAtom | |
| 304 | + | p.atoms[name] = a | |
| 305 | + | } | |
| 306 | + | p.reply(0, le32(a)) | |
| 307 | + | case r.major == opGetInputFocus: | |
| 308 | + | p.reply(1, le32(0)) | |
| 309 | + | case r.major == opQueryTree: | |
| 310 | + | b := make([]byte, 24+4*len(p.children)) | |
| 311 | + | binary.LittleEndian.PutUint32(b[0:], peerRoot) | |
| 312 | + | binary.LittleEndian.PutUint16(b[8:], uint16(len(p.children))) | |
| 313 | + | for i, c := range p.children { | |
| 314 | + | binary.LittleEndian.PutUint32(b[24+4*i:], c) | |
| 315 | + | } | |
| 316 | + | p.reply(0, b) | |
| 317 | + | case r.major == opGetWindowAttributes: | |
| 318 | + | a := p.attrs[binary.LittleEndian.Uint32(body)] | |
| 319 | + | b := make([]byte, 36) // reply length 3 | |
| 320 | + | b[18] = a[1] // map_state is byte 26 of the reply | |
| 321 | + | b[19] = a[0] // override_redirect, byte 27 | |
| 322 | + | p.reply(0, b) | |
| 323 | + | case r.major == opGetGeometry && p.bad[binary.LittleEndian.Uint32(body)]: | |
| 324 | + | p.errorPacket(9, r.major, binary.LittleEndian.Uint32(body)) // BadDrawable | |
| 325 | + | case r.major == opGetProperty && p.bad[binary.LittleEndian.Uint32(body)]: | |
| 326 | + | p.errorPacket(3, r.major, binary.LittleEndian.Uint32(body)) // BadWindow | |
| 327 | + | case r.major == opConfigureWindow: | |
| 328 | + | g := p.geoms[binary.LittleEndian.Uint32(body)] | |
| 329 | + | mask, vals := binary.LittleEndian.Uint16(body[4:]), body[8:] | |
| 330 | + | for bit, dst := 0, []any{&g.x, &g.y, &g.w, &g.h, &g.border}; bit < 5; bit++ { | |
| 331 | + | if mask&(1<<bit) == 0 { | |
| 332 | + | continue | |
| 333 | + | } | |
| 334 | + | v := uint16(binary.LittleEndian.Uint32(vals)) | |
| 335 | + | vals = vals[4:] | |
| 336 | + | switch d := dst[bit].(type) { | |
| 337 | + | case *int16: | |
| 338 | + | *d = int16(v) | |
| 339 | + | case *uint16: | |
| 340 | + | *d = v | |
| 341 | + | } | |
| 342 | + | } | |
| 343 | + | p.geoms[binary.LittleEndian.Uint32(body)] = g | |
| 344 | + | case r.major == opGetGeometry: | |
| 345 | + | g := p.geoms[binary.LittleEndian.Uint32(body)] | |
| 346 | + | b := make([]byte, 24) | |
| 347 | + | binary.LittleEndian.PutUint32(b[0:], peerRoot) | |
| 348 | + | binary.LittleEndian.PutUint16(b[4:], uint16(g.x)) | |
| 349 | + | binary.LittleEndian.PutUint16(b[6:], uint16(g.y)) | |
| 350 | + | binary.LittleEndian.PutUint16(b[8:], g.w) | |
| 351 | + | binary.LittleEndian.PutUint16(b[10:], g.h) | |
| 352 | + | binary.LittleEndian.PutUint16(b[12:], g.border) | |
| 353 | + | p.reply(24, b) | |
| 354 | + | case r.major == opChangeProperty: | |
| 355 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |
| 356 | + | format, n := body[12], int(binary.LittleEndian.Uint32(body[16:])) | |
| 357 | + | if p.props[win] == nil { | |
| 358 | + | p.props[win] = map[uint32]xpeerProp{} | |
| 359 | + | } | |
| 360 | + | p.props[win][prop] = xpeerProp{ | |
| 361 | + | typ: binary.LittleEndian.Uint32(body[8:]), format: format, | |
| 362 | + | data: append([]byte(nil), body[20:20+n*int(format)/8]...), | |
| 363 | + | } | |
| 364 | + | case r.major == opDeleteProperty: | |
| 365 | + | delete(p.props[binary.LittleEndian.Uint32(body)], binary.LittleEndian.Uint32(body[4:])) | |
| 366 | + | case r.major == opGetProperty: | |
| 367 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |
| 368 | + | v, ok := p.props[win][prop] | |
| 369 | + | b := make([]byte, 24+(len(v.data)+3)&^3) | |
| 370 | + | if ok && v.format != 0 { | |
| 371 | + | binary.LittleEndian.PutUint32(b[0:], v.typ) | |
| 372 | + | binary.LittleEndian.PutUint32(b[8:], uint32(len(v.data)/(int(v.format)/8))) | |
| 373 | + | copy(b[24:], v.data) | |
| 374 | + | } | |
| 375 | + | p.reply(v.format, b) | |
| 376 | + | case r.major == opChangeWindowAttributes: | |
| 377 | + | const cwEventMask = 1 << 11 | |
| 378 | + | if p.denyRoot && binary.LittleEndian.Uint32(body) == peerRoot && binary.LittleEndian.Uint32(body[4:])&cwEventMask != 0 { | |
| 379 | + | p.errorPacket(10, opChangeWindowAttributes, peerRoot) // BadAccess | |
| 380 | + | } | |
| 381 | + | case r.major >= 128: | |
| 382 | + | switch p.extName(r.major) { | |
| 383 | + | case "BIG-REQUESTS": | |
| 384 | + | p.reply(0, le32(peerMaxWords)) | |
| 385 | + | case "XC-MISC": | |
| 386 | + | if r.minor == 1 { | |
| 387 | + | p.reply(0, le32(0, 1)) | |
| 388 | + | } | |
| 389 | + | case "Composite", "XFIXES": | |
| 390 | + | if r.minor == 0 { | |
| 391 | + | p.reply(0, le32(5, 4)) | |
| 392 | + | } | |
| 393 | + | case "X-Resource": | |
| 394 | + | switch r.minor { | |
| 395 | + | case 0: | |
| 396 | + | p.reply(0, []byte{1, 0, 2, 0}) | |
| 397 | + | case 4: // QueryClientIds: one spec, answered when a pid is scripted | |
| 398 | + | pid, ok := p.pids[r.u32(8)] | |
| 399 | + | if !ok { | |
| 400 | + | p.reply(0, le32(0)) | |
| 401 | + | break | |
| 402 | + | } | |
| 403 | + | b := make([]byte, 40) | |
| 404 | + | binary.LittleEndian.PutUint32(b[0:], 1) | |
| 405 | + | binary.LittleEndian.PutUint32(b[24:], r.u32(8)) | |
| 406 | + | binary.LittleEndian.PutUint32(b[28:], 2) // LocalClientPID | |
| 407 | + | binary.LittleEndian.PutUint32(b[32:], 4) | |
| 408 | + | binary.LittleEndian.PutUint32(b[36:], pid) | |
| 409 | + | p.reply(0, b) | |
| 410 | + | } | |
| 411 | + | } | |
| 412 | + | } | |
| 413 | + | } | |
| 414 | + | ||
| 415 | + | // requests returns a snapshot of every request received after setup. | |
| 416 | + | func (p *xpeer) requests() []xpeerReq { | |
| 417 | + | p.mu.Lock() | |
| 418 | + | defer p.mu.Unlock() | |
| 419 | + | return append([]xpeerReq(nil), p.reqs...) | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | // waitReq waits for a request satisfying match among those after the first | |
| 423 | + | // skip requests, and returns it with the new count. | |
| 424 | + | func (p *xpeer) waitReq(skip int, match func(xpeerReq) bool) (xpeerReq, int) { | |
| 425 | + | p.t.Helper() | |
| 426 | + | deadline := time.Now().Add(5 * time.Second) | |
| 427 | + | for time.Now().Before(deadline) { | |
| 428 | + | reqs := p.requests() | |
| 429 | + | for i := skip; i < len(reqs); i++ { | |
| 430 | + | if match(reqs[i]) { | |
| 431 | + | return reqs[i], i + 1 | |
| 432 | + | } | |
| 433 | + | } | |
| 434 | + | time.Sleep(time.Millisecond) | |
| 435 | + | } | |
| 436 | + | p.t.Fatal("xpeer: expected request never arrived") | |
| 437 | + | return xpeerReq{}, 0 | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | func (p *xpeer) atom(name string) uint32 { | |
| 441 | + | p.mu.Lock() | |
| 442 | + | defer p.mu.Unlock() | |
| 443 | + | return p.atoms[name] | |
| 444 | + | } | |
| 445 | + | ||
| 446 | + | func (p *xpeer) prop(win uint32, atom string) (xpeerProp, bool) { | |
| 447 | + | p.mu.Lock() | |
| 448 | + | defer p.mu.Unlock() | |
| 449 | + | v, ok := p.props[win][p.atoms[atom]] | |
| 450 | + | return v, ok | |
| 451 | + | } | |
| 452 | + | ||
| 453 | + | // event sends a 32-byte core event stamped with the current sequence number. | |
| 454 | + | func (p *xpeer) event(code uint8, fill func(b *[32]byte)) { | |
| 455 | + | p.mu.Lock() | |
| 456 | + | defer p.mu.Unlock() | |
| 457 | + | var b [32]byte | |
| 458 | + | b[0] = code | |
| 459 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |
| 460 | + | if fill != nil { | |
| 461 | + | fill(&b) | |
| 462 | + | } | |
| 463 | + | p.write(b[:]) | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | func (p *xpeer) createNotify(win uint32, override bool) { | |
| 467 | + | p.event(16, func(b *[32]byte) { | |
| 468 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 469 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 470 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |
| 471 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |
| 472 | + | if override { | |
| 473 | + | b[22] = 1 | |
| 474 | + | } | |
| 475 | + | }) | |
| 476 | + | } | |
| 477 | + | ||
| 478 | + | func (p *xpeer) destroyNotify(win uint32) { | |
| 479 | + | p.event(17, func(b *[32]byte) { | |
| 480 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 481 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 482 | + | }) | |
| 483 | + | } | |
| 484 | + | ||
| 485 | + | // pause makes the peer stop reading after the request it is serving. | |
| 486 | + | func (p *xpeer) pause() { | |
| 487 | + | g := make(chan struct{}) | |
| 488 | + | p.gate.Store(&g) | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | // resume lets the peer read again. | |
| 492 | + | func (p *xpeer) resume() { | |
| 493 | + | g := *p.gate.Load() | |
| 494 | + | open := make(chan struct{}) | |
| 495 | + | close(open) | |
| 496 | + | p.gate.Store(&open) | |
| 497 | + | select { | |
| 498 | + | case <-g: | |
| 499 | + | default: | |
| 500 | + | close(g) | |
| 501 | + | } | |
| 502 | + | } | |
| 503 | + | ||
| 504 | + | // set mutates the peer's script under its lock. | |
| 505 | + | func (p *xpeer) set(fn func(p *xpeer)) { | |
| 506 | + | p.mu.Lock() | |
| 507 | + | defer p.mu.Unlock() | |
| 508 | + | fn(p) | |
| 509 | + | } | |
| 510 | + | ||
| 511 | + | // hangup closes the server end; the client reads EOF. | |
| 512 | + | func (p *xpeer) hangup() { | |
| 513 | + | if rc, err := p.f.SyscallConn(); err == nil { | |
| 514 | + | _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) }) | |
| 515 | + | } | |
| 516 | + | } | |
| 517 | + | ||
| 518 | + | // setProp scripts a window property for GetProperty. | |
| 519 | + | func (p *xpeer) setProp(win, atom, typ uint32, format uint8, data []byte) { | |
| 520 | + | p.mu.Lock() | |
| 521 | + | defer p.mu.Unlock() | |
| 522 | + | if p.props[win] == nil { | |
| 523 | + | p.props[win] = map[uint32]xpeerProp{} | |
| 524 | + | } | |
| 525 | + | p.props[win][atom] = xpeerProp{typ: typ, format: format, data: data} | |
| 526 | + | } | |
| 527 | + | ||
| 528 | + | func (p *xpeer) mapRequest(win uint32) { | |
| 529 | + | p.event(20, func(b *[32]byte) { | |
| 530 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 531 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 532 | + | }) | |
| 533 | + | } | |
| 534 | + | ||
| 535 | + | func (p *xpeer) mapNotify(win uint32) { | |
| 536 | + | p.event(19, func(b *[32]byte) { | |
| 537 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 538 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 539 | + | }) | |
| 540 | + | } | |
| 541 | + | ||
| 542 | + | func (p *xpeer) unmapNotify(win uint32, synthetic bool) { | |
| 543 | + | p.event(18, func(b *[32]byte) { | |
| 544 | + | if synthetic { | |
| 545 | + | b[0] |= 0x80 | |
| 546 | + | } | |
| 547 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 548 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 549 | + | }) | |
| 550 | + | } | |
| 551 | + | ||
| 552 | + | func (p *xpeer) propertyNotify(win, atom uint32, deleted bool) { | |
| 553 | + | p.event(28, func(b *[32]byte) { | |
| 554 | + | binary.LittleEndian.PutUint32(b[4:], win) | |
| 555 | + | binary.LittleEndian.PutUint32(b[8:], atom) | |
| 556 | + | if deleted { | |
| 557 | + | b[16] = 1 | |
| 558 | + | } | |
| 559 | + | }) | |
| 560 | + | } | |
| 561 | + | ||
| 562 | + | func (p *xpeer) reparentNotify(win, parent uint32) { | |
| 563 | + | p.event(21, func(b *[32]byte) { | |
| 564 | + | binary.LittleEndian.PutUint32(b[4:], peerRoot) | |
| 565 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 566 | + | binary.LittleEndian.PutUint32(b[12:], parent) | |
| 567 | + | }) | |
| 568 | + | } | |
| 569 | + | ||
| 570 | + | func (p *xpeer) clientMessage(win, typ uint32, data ...uint32) { | |
| 571 | + | p.event(33, func(b *[32]byte) { | |
| 572 | + | b[1] = 32 | |
| 573 | + | binary.LittleEndian.PutUint32(b[4:], win) | |
| 574 | + | binary.LittleEndian.PutUint32(b[8:], typ) | |
| 575 | + | for i, d := range data { | |
| 576 | + | binary.LittleEndian.PutUint32(b[12+4*i:], d) | |
| 577 | + | } | |
| 578 | + | }) | |
| 579 | + | } | |
| 580 | + | ||
| 581 | + | func (p *xpeer) focusIn(win uint32) { | |
| 582 | + | p.event(9, func(b *[32]byte) { binary.LittleEndian.PutUint32(b[4:], win) }) | |
| 583 | + | } | |
| 584 | + | ||
| 585 | + | func (p *xpeer) configureNotify(win uint32) { | |
| 586 | + | p.event(22, func(b *[32]byte) { | |
| 587 | + | binary.LittleEndian.PutUint32(b[4:], win) | |
| 588 | + | binary.LittleEndian.PutUint32(b[8:], win) | |
| 589 | + | binary.LittleEndian.PutUint16(b[20:], 10) | |
| 590 | + | binary.LittleEndian.PutUint16(b[22:], 20) | |
| 591 | + | binary.LittleEndian.PutUint16(b[24:], 300) | |
| 592 | + | binary.LittleEndian.PutUint16(b[26:], 200) | |
| 593 | + | }) | |
| 594 | + | } | |
A internal/adapters/wayland/xwm/xwm.go +438 -0
| @@ -0,0 +1,438 @@ | |||
| 1 | + | // Package xwm is the built-in X11 window manager for Xwayland: it speaks the | |
| 2 | + | // X protocol over the -wm socket with xturbo and reports managed windows to | |
| 3 | + | // the Wayland server through the unexported host interface. | |
| 4 | + | // | |
| 5 | + | // Threading: Boot runs on the starter goroutine and blocks. The XWM it returns | |
| 6 | + | // is then handed to the display goroutine, which owns it exclusively; nothing | |
| 7 | + | // in this package is locked. | |
| 8 | + | package xwm | |
| 9 | + | ||
| 10 | + | import ( | |
| 11 | + | "context" | |
| 12 | + | "encoding/binary" | |
| 13 | + | "errors" | |
| 14 | + | "fmt" | |
| 15 | + | "io" | |
| 16 | + | "os" | |
| 17 | + | ||
| 18 | + | "bnema.dev/xturbo" | |
| 19 | + | "bnema.dev/xturbo/composite" | |
| 20 | + | "bnema.dev/xturbo/res" | |
| 21 | + | "bnema.dev/xturbo/xfixes" | |
| 22 | + | "bnema.dev/xturbo/xproto" | |
| 23 | + | "github.com/bnema/purego-libwayland/server" | |
| 24 | + | "github.com/bnema/zerowrap" | |
| 25 | + | "golang.org/x/sys/unix" | |
| 26 | + | ) | |
| 27 | + | ||
| 28 | + | var ( | |
| 29 | + | // ErrWMExists is returned by Boot when another client already redirects | |
| 30 | + | // the root window's children. | |
| 31 | + | ErrWMExists = errors.New("xwm: another window manager owns the root window") | |
| 32 | + | // ErrNoComposite is returned by Boot when the X server lacks Composite. | |
| 33 | + | ErrNoComposite = errors.New("xwm: X server has no Composite extension") | |
| 34 | + | ) | |
| 35 | + | ||
| 36 | + | // Stats are connection and event counters, for diagnostics. | |
| 37 | + | type Stats struct { | |
| 38 | + | Events, Replies, Errors, Unattributed, Stale, RingFull, LegacyID, UnmanagedSeen uint64 | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Version request ids, carried in the first Expect argument of kindVersion. | |
| 42 | + | const ( | |
| 43 | + | versionComposite uint32 = iota + 1 | |
| 44 | + | versionXFixes | |
| 45 | + | versionRes | |
| 46 | + | ) | |
| 47 | + | ||
| 48 | + | const unpairedCap = 64 | |
| 49 | + | ||
| 50 | + | // XWM manages the windows of one Xwayland. Display goroutine only, after Boot. | |
| 51 | + | type XWM struct { | |
| 52 | + | conn *xturbo.Conn | |
| 53 | + | log zerowrap.Logger | |
| 54 | + | root uint32 | |
| 55 | + | wmWin uint32 | |
| 56 | + | atoms [atomCount]uint32 | |
| 57 | + | tab *table | |
| 58 | + | h xturbo.Handler | |
| 59 | + | stats Stats | |
| 60 | + | ||
| 61 | + | // attachment | |
| 62 | + | host host | |
| 63 | + | src fdSource | |
| 64 | + | post func(func()) | |
| 65 | + | flushFn func() | |
| 66 | + | ||
| 67 | + | posted, writable, closed bool | |
| 68 | + | bootErr error | |
| 69 | + | warnedRing, warnedFull bool | |
| 70 | + | warnedUnpaired bool | |
| 71 | + | ||
| 72 | + | scratch []byte // encode buffer for 32-bit property values, 4 bytes per slot | |
| 73 | + | ||
| 74 | + | // Shared by the map, configure and focus flows. | |
| 75 | + | dirty []Slot // slots with a pending configure, capacity == table size | |
| 76 | + | clients []uint32 // _NET_CLIENT_LIST, map order | |
| 77 | + | stacking []uint32 // _NET_CLIENT_LIST_STACKING, bottom to top | |
| 78 | + | active Slot // slot named by _NET_ACTIVE_WINDOW, 0 = none | |
| 79 | + | refocus bool // a FocusIn was denied; focus is re-issued in flush | |
| 80 | + | listsDirty uint8 // client lists to rewrite in flush | |
| 81 | + | unpaired [unpairedCap]struct{ serial, surface uint64 } | |
| 82 | + | unpairedN int | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM { | |
| 86 | + | x := &XWM{conn: conn, log: log, root: conn.Root(), tab: newTable(initialSlots)} | |
| 87 | + | x.h = xturbo.Handler{OnEvent: x.onEvent, OnReply: x.onReply, OnError: x.onError} | |
| 88 | + | x.fit() | |
| 89 | + | return x | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | // fit sizes the per-slot buffers to the table; called after every insert. | |
| 93 | + | func (x *XWM) fit() { | |
| 94 | + | n := len(x.tab.recs) | |
| 95 | + | if cap(x.dirty) < n { | |
| 96 | + | x.dirty = append(make([]Slot, 0, n), x.dirty...) | |
| 97 | + | x.clients = append(make([]uint32, 0, n), x.clients...) | |
| 98 | + | x.stacking = append(make([]uint32, 0, n), x.stacking...) | |
| 99 | + | x.scratch = make([]byte, 0, 4*n) | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | // insert adds a record for xid; 0 means the table is full. | |
| 104 | + | func (x *XWM) insert(xid uint32) Slot { | |
| 105 | + | s := x.tab.insert(xid) | |
| 106 | + | if s == 0 { | |
| 107 | + | if !x.warnedFull { | |
| 108 | + | x.warnedFull = true | |
| 109 | + | x.log.Warn().Int("windows", x.tab.n).Msg("X11 window table full, window ignored") | |
| 110 | + | } | |
| 111 | + | return 0 | |
| 112 | + | } | |
| 113 | + | x.fit() | |
| 114 | + | return s | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // expect registers seq, tolerating a full ring: the reply then arrives | |
| 118 | + | // unattributed and is dropped. | |
| 119 | + | func (x *XWM) expect(seq xturbo.Seq, kind uint16, a0, a1 uint32) { | |
| 120 | + | if seq == 0 { | |
| 121 | + | return | |
| 122 | + | } | |
| 123 | + | if err := x.conn.Expect(seq, kind, a0, a1, 0, 0); err != nil { | |
| 124 | + | x.stats.RingFull++ | |
| 125 | + | if !x.warnedRing { | |
| 126 | + | x.warnedRing = true | |
| 127 | + | x.log.Warn().Err(err).Msg("X11 request not tracked") | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // setProp32 queues ChangeProperty (replace, format 32) for vals. | |
| 133 | + | func (x *XWM) setProp32(win uint32, prop int, typ xproto.Atom, vals []uint32) { | |
| 134 | + | n := 4 * len(vals) | |
| 135 | + | if cap(x.scratch) < n { | |
| 136 | + | x.scratch = make([]byte, 0, n) | |
| 137 | + | } | |
| 138 | + | p := x.scratch[:n] | |
| 139 | + | for i, v := range vals { | |
| 140 | + | binary.LittleEndian.PutUint32(p[4*i:], v) | |
| 141 | + | } | |
| 142 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(win), | |
| 143 | + | xproto.Atom(x.atoms[prop]), typ, 32, uint32(len(vals)), p) | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | // Boot connects to the X server on wmFD and becomes its window manager. It | |
| 147 | + | // blocks (about 25 s at worst) and must not run on the display goroutine: the | |
| 148 | + | // handshake needs Xwayland, which needs the display goroutine to serve its | |
| 149 | + | // Wayland requests. It takes ownership of wmFD and closes it on failure. | |
| 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) { | |
| 189 | + | conn, err := xturbo.NewConn(wmFD) | |
| 190 | + | if err != nil { | |
| 191 | + | return nil, fmt.Errorf("xwm: connect: %w", err) | |
| 192 | + | } | |
| 193 | + | x := newXWM(conn, log) | |
| 194 | + | if err := x.boot(); err != nil { | |
| 195 | + | _ = conn.Close() | |
| 196 | + | return nil, err | |
| 197 | + | } | |
| 198 | + | return x, nil | |
| 199 | + | } | |
| 200 | + | ||
| 201 | + | func (x *XWM) boot() error { | |
| 202 | + | conn := x.conn | |
| 203 | + | ext := conn.Ext() | |
| 204 | + | if !ext.Info(xturbo.ExtComposite).Present { | |
| 205 | + | return ErrNoComposite | |
| 206 | + | } | |
| 207 | + | var visual, depth uint32 | |
| 208 | + | if it := xproto.NewSetupIter(conn.SetupBytes(), 1); it.Next() { | |
| 209 | + | if roots := it.Value().Roots(); roots.Next() { | |
| 210 | + | visual, depth = uint32(roots.Value().RootVisual()), uint32(roots.Value().RootDepth()) | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | x.log.Info().Uint32("root", x.root).Uint32("visual", visual).Uint32("depth", depth). | |
| 214 | + | Bool("xfixes", ext.Info(xturbo.ExtXFixes).Present). | |
| 215 | + | Bool("res", ext.Info(xturbo.ExtRes).Present).Msg("X server") | |
| 216 | + | ||
| 217 | + | if err := conn.InternAtoms(atomNames[:], false, x.atoms[:]); err != nil { | |
| 218 | + | return fmt.Errorf("xwm: intern atoms: %w", err) | |
| 219 | + | } | |
| 220 | + | xid, err := conn.NewXID() | |
| 221 | + | if err != nil { | |
| 222 | + | return fmt.Errorf("xwm: allocate window id: %w", err) | |
| 223 | + | } | |
| 224 | + | x.wmWin = xid | |
| 225 | + | ||
| 226 | + | b := conn.Buf() | |
| 227 | + | x.expect(composite.QueryVersion(b, 0, 4), kindVersion, versionComposite, 0) | |
| 228 | + | if ext.Info(xturbo.ExtXFixes).Present { | |
| 229 | + | x.expect(xfixes.QueryVersion(b, 6, 0), kindVersion, versionXFixes, 0) | |
| 230 | + | } | |
| 231 | + | if ext.Info(xturbo.ExtRes).Present { | |
| 232 | + | x.expect(res.QueryVersion(b, 1, 2), kindVersion, versionRes, 0) | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | root := xproto.Window(x.root) | |
| 236 | + | var rootMask xproto.ChangeWindowAttributesValues | |
| 237 | + | rootMask.SetEventMask(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange) | |
| 238 | + | x.expect(xproto.ChangeWindowAttributes(b, root, &rootMask), kindRootMask, 0, 0) | |
| 239 | + | // Only one client may select SubstructureRedirect on the root: wait for | |
| 240 | + | // the answer before touching anything another window manager owns. | |
| 241 | + | if err := b.Err(); err != nil { | |
| 242 | + | return fmt.Errorf("xwm: encode root mask: %w", err) | |
| 243 | + | } | |
| 244 | + | if err := conn.Sync(&x.h); err != nil { | |
| 245 | + | return fmt.Errorf("xwm: root mask: %w", err) | |
| 246 | + | } | |
| 247 | + | if x.bootErr != nil { | |
| 248 | + | return x.bootErr | |
| 249 | + | } | |
| 250 | + | composite.RedirectSubwindows(b, root, composite.RedirectManual) | |
| 251 | + | ||
| 252 | + | var wv xproto.CreateWindowValues | |
| 253 | + | wv.SetEventMask(0) | |
| 254 | + | xproto.CreateWindow(b, 0, xproto.Window(x.wmWin), root, -1, -1, 1, 1, 0, xproto.WindowClassInputOutput, 0, &wv) | |
| 255 | + | name := []byte("neferwl") | |
| 256 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMName]), | |
| 257 | + | xproto.Atom(x.atoms[atomUTF8String]), 8, uint32(len(name)), name) | |
| 258 | + | x.setProp32(x.root, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin}) | |
| 259 | + | x.setProp32(x.wmWin, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin}) | |
| 260 | + | // CurrentTime and no MANAGER announcement: this Xwayland is private to | |
| 261 | + | // neferwl and nobody waits on these selections. | |
| 262 | + | xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomWMS0]), 0) | |
| 263 | + | xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMCMS0]), 0) | |
| 264 | + | ||
| 265 | + | supported := [...]int{ | |
| 266 | + | atomNetWMState, atomNetActiveWindow, atomNetWMStateFocused, | |
| 267 | + | atomNetWMStateModal, atomNetWMStateFullscreen, atomNetClientList, | |
| 268 | + | atomNetClientListStacking, atomNetSupportingWMCheck, atomNetWMName, atomNetWMPID, | |
| 269 | + | atomNetWMWindowType, | |
| 270 | + | } | |
| 271 | + | var list [len(supported)]uint32 | |
| 272 | + | for i, a := range supported { | |
| 273 | + | list[i] = x.atoms[a] | |
| 274 | + | } | |
| 275 | + | x.setProp32(x.root, atomNetSupported, xproto.AtomAtom, list[:]) | |
| 276 | + | x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, []uint32{0}) | |
| 277 | + | x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, nil) | |
| 278 | + | x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, nil) | |
| 279 | + | xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0) | |
| 280 | + | if err := b.Err(); err != nil { | |
| 281 | + | return fmt.Errorf("xwm: encode setup requests: %w", err) | |
| 282 | + | } | |
| 283 | + | if err := conn.Sync(&x.h); err != nil { | |
| 284 | + | return fmt.Errorf("xwm: root setup: %w", err) | |
| 285 | + | } | |
| 286 | + | if x.bootErr != nil { | |
| 287 | + | return x.bootErr | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | // Windows that exist already (Xwayland restarted under a live client set). | |
| 291 | + | x.expect(xproto.QueryTree(b, root), kindTree, 0, 0) | |
| 292 | + | if err := conn.Sync(&x.h); err != nil { | |
| 293 | + | return fmt.Errorf("xwm: query tree: %w", err) | |
| 294 | + | } | |
| 295 | + | return x.bootErr | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | // Attach registers the X socket with the display and hands the windows found | |
| 299 | + | // at boot to h. Display goroutine only. | |
| 300 | + | func (x *XWM) Attach(d *server.Display, h host) error { | |
| 301 | + | src, err := d.AddFDRW(x.conn.Fd(), x.onReadable, x.onWritable) | |
| 302 | + | if err != nil { | |
| 303 | + | return fmt.Errorf("xwm: watch X socket: %w", err) | |
| 304 | + | } | |
| 305 | + | x.attach(src, h, d.Post) | |
| 306 | + | return nil | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | // attach is Attach over its seams: the event source and the display's Post. | |
| 310 | + | func (x *XWM) attach(src fdSource, h host, post func(func())) { | |
| 311 | + | x.src, x.host, x.post = src, h, post | |
| 312 | + | x.flushFn = func() { | |
| 313 | + | x.posted = false | |
| 314 | + | x.flush() | |
| 315 | + | } | |
| 316 | + | for i := range x.tab.recs { | |
| 317 | + | r := &x.tab.recs[i] | |
| 318 | + | if r.kind != kindManaged || r.id != 0 { | |
| 319 | + | continue | |
| 320 | + | } | |
| 321 | + | s := Slot(i + 1) | |
| 322 | + | x.adopt(s) | |
| 323 | + | if r.mapping { // MapRequest or viewable before Attach | |
| 324 | + | r.mapping = false | |
| 325 | + | x.beginMap(s) | |
| 326 | + | } | |
| 327 | + | } | |
| 328 | + | x.flush() | |
| 329 | + | } | |
| 330 | + | ||
| 331 | + | // Close detaches from the display, drops every window from the host and closes | |
| 332 | + | // the X connection. Idempotent. | |
| 333 | + | func (x *XWM) Close() { | |
| 334 | + | if x.closed { | |
| 335 | + | return | |
| 336 | + | } | |
| 337 | + | x.closed = true | |
| 338 | + | s := x.stats | |
| 339 | + | x.log.Info().Uint64("events", s.Events).Uint64("replies", s.Replies).Uint64("errors", s.Errors). | |
| 340 | + | Uint64("unattributed", s.Unattributed).Uint64("stale", s.Stale).Uint64("ring_full", s.RingFull). | |
| 341 | + | Uint64("legacy_id", s.LegacyID).Uint64("unmanaged_seen", s.UnmanagedSeen).Msg("X11 window management closed") | |
| 342 | + | if x.src != nil { | |
| 343 | + | x.src.Remove() | |
| 344 | + | x.src = nil | |
| 345 | + | } | |
| 346 | + | if x.host != nil { | |
| 347 | + | for i := range x.tab.recs { | |
| 348 | + | if r := &x.tab.recs[i]; r.kind == kindManaged && r.id != 0 { | |
| 349 | + | r.id = 0 | |
| 350 | + | x.host.Dissociate(Slot(i+1), true) | |
| 351 | + | } | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | x.unpairedN = 0 | |
| 355 | + | _ = x.conn.Close() | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | // CloseConn closes the connection of an XWM that was never attached. | |
| 359 | + | func (x *XWM) CloseConn() { | |
| 360 | + | if !x.closed { | |
| 361 | + | x.closed = true | |
| 362 | + | _ = x.conn.Close() | |
| 363 | + | } | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | // fail ends window management after an unrecoverable connection error. Xwayland | |
| 367 | + | // keeps running; its windows no longer map. | |
| 368 | + | func (x *XWM) fail(err error) { | |
| 369 | + | if x.closed { | |
| 370 | + | return | |
| 371 | + | } | |
| 372 | + | if errors.Is(err, io.EOF) || errors.Is(err, xturbo.ErrClosed) { | |
| 373 | + | x.log.Info().Err(err).Msg("X11 window management stopped: Xwayland closed the connection") | |
| 374 | + | } else { | |
| 375 | + | x.log.Warn().Err(err).Msg("X11 window management stopped") | |
| 376 | + | } | |
| 377 | + | x.Close() | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | // Stats returns the counters; Close logs them. | |
| 381 | + | func (x *XWM) Stats() Stats { return x.stats } | |
| 382 | + | ||
| 383 | + | func (x *XWM) onReadable() { | |
| 384 | + | if x.closed { | |
| 385 | + | return | |
| 386 | + | } | |
| 387 | + | if err := x.conn.Dispatch(&x.h); err != nil { | |
| 388 | + | x.fail(err) | |
| 389 | + | return | |
| 390 | + | } | |
| 391 | + | x.flush() | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | func (x *XWM) onWritable() { x.flush() } | |
| 395 | + | ||
| 396 | + | // wake schedules one flush at the end of the current display iteration. | |
| 397 | + | func (x *XWM) wake() { | |
| 398 | + | if x.posted || x.closed || x.post == nil { | |
| 399 | + | return | |
| 400 | + | } | |
| 401 | + | x.posted = true | |
| 402 | + | x.post(x.flushFn) | |
| 403 | + | } | |
| 404 | + | ||
| 405 | + | // flush sends what the handlers queued: pending configures, an XID prefetch, | |
| 406 | + | // then the socket, arming POLLOUT only while it is full. | |
| 407 | + | func (x *XWM) flush() { | |
| 408 | + | if x.closed { | |
| 409 | + | return | |
| 410 | + | } | |
| 411 | + | x.flushConfigures() | |
| 412 | + | if x.conn.XIDsLow() { | |
| 413 | + | _ = x.conn.PrefetchXIDs() | |
| 414 | + | } | |
| 415 | + | err := x.conn.Flush() | |
| 416 | + | switch { | |
| 417 | + | case err == nil: | |
| 418 | + | if x.writable { | |
| 419 | + | x.setWritable(false) | |
| 420 | + | } | |
| 421 | + | case errors.Is(err, xturbo.ErrWouldBlock): | |
| 422 | + | if !x.writable { | |
| 423 | + | x.setWritable(true) | |
| 424 | + | } | |
| 425 | + | default: | |
| 426 | + | x.fail(err) | |
| 427 | + | } | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | func (x *XWM) setWritable(on bool) { | |
| 431 | + | x.writable = on | |
| 432 | + | if x.src == nil { | |
| 433 | + | return | |
| 434 | + | } | |
| 435 | + | if err := x.src.SetWritable(on); err != nil { | |
| 436 | + | x.fail(err) | |
| 437 | + | } | |
| 438 | + | } | |
A internal/adapters/wayland/xwm/xwm_test.go +381 -0
| @@ -0,0 +1,381 @@ | |||
| 1 | + | package xwm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "os" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "bnema.dev/xturbo/xproto" | |
| 12 | + | "github.com/bnema/neferwl/internal/ports" | |
| 13 | + | "github.com/bnema/zerowrap" | |
| 14 | + | "github.com/rs/zerolog" | |
| 15 | + | "github.com/stretchr/testify/mock" | |
| 16 | + | "github.com/stretchr/testify/require" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | func testLog() zerowrap.Logger { return zerowrap.Logger{Logger: zerolog.Nop()} } | |
| 20 | + | ||
| 21 | + | func bootPeer(t *testing.T, mutate func(p *xpeer)) (*XWM, *xpeer) { | |
| 22 | + | t.Helper() | |
| 23 | + | fd, p := startXPeer(t, fullPeerExts()) | |
| 24 | + | if mutate != nil { | |
| 25 | + | p.set(mutate) | |
| 26 | + | } | |
| 27 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 28 | + | require.NoError(t, err) | |
| 29 | + | t.Cleanup(x.CloseConn) | |
| 30 | + | return x, p | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func atomsOf(p xpeerProp) []uint32 { | |
| 34 | + | out := make([]uint32, len(p.data)/4) | |
| 35 | + | for i := range out { | |
| 36 | + | out[i] = binary.LittleEndian.Uint32(p.data[4*i:]) | |
| 37 | + | } | |
| 38 | + | return out | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func TestBootRequests(t *testing.T) { | |
| 42 | + | x, p := bootPeer(t, nil) | |
| 43 | + | reqs := p.requests() | |
| 44 | + | ||
| 45 | + | first := -1 | |
| 46 | + | for i, r := range reqs { | |
| 47 | + | if r.major == opInternAtom { | |
| 48 | + | first = i | |
| 49 | + | break | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | require.NotEqual(t, -1, first) | |
| 53 | + | for i := 0; i < atomCount; i++ { | |
| 54 | + | r := reqs[first+i] | |
| 55 | + | require.EqualValues(t, opInternAtom, r.major, i) | |
| 56 | + | n := int(binary.LittleEndian.Uint16(r.raw[4:])) | |
| 57 | + | require.Equal(t, atomNames[i], string(r.raw[8:8+n]), i) | |
| 58 | + | require.Equal(t, p.atom(atomNames[i]), x.atoms[i]) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // Root mask, then composite redirect, then the WM window and ownership. | |
| 62 | + | var rootMask, redirect, create, s0, cm, focus int | |
| 63 | + | rootMask, redirect, create, s0, cm, focus = -1, -1, -1, -1, -1, -1 | |
| 64 | + | wm := x.wmWin | |
| 65 | + | for i := first + atomCount; i < len(reqs); i++ { | |
| 66 | + | r := reqs[i] | |
| 67 | + | switch { | |
| 68 | + | case r.major == opChangeWindowAttributes && r.u32(4) == peerRoot: | |
| 69 | + | rootMask = i | |
| 70 | + | require.EqualValues(t, 1<<11, r.u32(8)) | |
| 71 | + | want := uint32(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange) | |
| 72 | + | require.Equal(t, want, r.u32(12)) | |
| 73 | + | case r.major == 142 && r.minor == 2: | |
| 74 | + | redirect = i | |
| 75 | + | require.EqualValues(t, peerRoot, r.u32(4)) | |
| 76 | + | require.EqualValues(t, 1, r.raw[8], "manual") | |
| 77 | + | case r.major == opCreateWindow: | |
| 78 | + | create = i | |
| 79 | + | require.Equal(t, wm, r.u32(4)) | |
| 80 | + | case r.major == opSetSelectionOwner && r.u32(8) == p.atom("WM_S0"): | |
| 81 | + | s0 = i | |
| 82 | + | require.Equal(t, wm, r.u32(4)) | |
| 83 | + | case r.major == opSetSelectionOwner && r.u32(8) == p.atom("_NET_WM_CM_S0"): | |
| 84 | + | cm = i | |
| 85 | + | case r.major == opSetInputFocus: | |
| 86 | + | focus = i | |
| 87 | + | require.EqualValues(t, 0, r.u32(4), "focus None") | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | for name, i := range map[string]int{"rootMask": rootMask, "redirect": redirect, "create": create, "WM_S0": s0, "CM_S0": cm, "focus": focus} { | |
| 91 | + | require.NotEqual(t, -1, i, name) | |
| 92 | + | } | |
| 93 | + | require.Less(t, rootMask, redirect) | |
| 94 | + | require.Less(t, redirect, create) | |
| 95 | + | require.Less(t, create, s0) | |
| 96 | + | ||
| 97 | + | sup, ok := p.prop(peerRoot, "_NET_SUPPORTED") | |
| 98 | + | require.True(t, ok) | |
| 99 | + | var names []string | |
| 100 | + | byAtom := map[uint32]string{} | |
| 101 | + | for n, a := range p.atoms { | |
| 102 | + | byAtom[a] = n | |
| 103 | + | } | |
| 104 | + | for _, a := range atomsOf(sup) { | |
| 105 | + | names = append(names, byAtom[a]) | |
| 106 | + | } | |
| 107 | + | require.Equal(t, []string{ | |
| 108 | + | "_NET_WM_STATE", "_NET_ACTIVE_WINDOW", "_NET_WM_STATE_FOCUSED", | |
| 109 | + | "_NET_WM_STATE_MODAL", "_NET_WM_STATE_FULLSCREEN", "_NET_CLIENT_LIST", | |
| 110 | + | "_NET_CLIENT_LIST_STACKING", "_NET_SUPPORTING_WM_CHECK", "_NET_WM_NAME", "_NET_WM_PID", | |
| 111 | + | "_NET_WM_WINDOW_TYPE", | |
| 112 | + | }, names) | |
| 113 | + | require.NotContains(t, names, "_NET_WM_MOVERESIZE") | |
| 114 | + | require.NotContains(t, names, "_NET_WM_STATE_MAXIMIZED_VERT") | |
| 115 | + | ||
| 116 | + | check, ok := p.prop(peerRoot, "_NET_SUPPORTING_WM_CHECK") | |
| 117 | + | require.True(t, ok) | |
| 118 | + | require.Equal(t, []uint32{wm}, atomsOf(check)) | |
| 119 | + | check, ok = p.prop(wm, "_NET_SUPPORTING_WM_CHECK") | |
| 120 | + | require.True(t, ok) | |
| 121 | + | require.Equal(t, []uint32{wm}, atomsOf(check)) | |
| 122 | + | name, ok := p.prop(wm, "_NET_WM_NAME") | |
| 123 | + | require.True(t, ok) | |
| 124 | + | require.Equal(t, "neferwl", string(name.data)) | |
| 125 | + | active, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW") | |
| 126 | + | require.True(t, ok) | |
| 127 | + | require.Equal(t, []uint32{0}, atomsOf(active)) | |
| 128 | + | for _, l := range []string{"_NET_CLIENT_LIST", "_NET_CLIENT_LIST_STACKING"} { | |
| 129 | + | v, ok := p.prop(peerRoot, l) | |
| 130 | + | require.True(t, ok, l) | |
| 131 | + | require.Empty(t, v.data, l) | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func TestBootWMExists(t *testing.T) { | |
| 136 | + | fd, p := startXPeer(t, fullPeerExts()) | |
| 137 | + | p.set(func(p *xpeer) { p.denyRoot = true }) | |
| 138 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 139 | + | require.ErrorIs(t, err, ErrWMExists) | |
| 140 | + | require.Nil(t, x) | |
| 141 | + | // Nothing the running window manager owns was touched. | |
| 142 | + | for _, r := range p.requests() { | |
| 143 | + | require.NotEqual(t, uint8(opSetSelectionOwner), r.major) | |
| 144 | + | if r.major == opChangeProperty { | |
| 145 | + | require.NotEqual(t, uint32(peerRoot), r.u32(4)) | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | ||
| 150 | + | func TestBootNoComposite(t *testing.T) { | |
| 151 | + | exts := fullPeerExts() | |
| 152 | + | delete(exts, "Composite") | |
| 153 | + | fd, _ := startXPeer(t, exts) | |
| 154 | + | _, err := Boot(context.Background(), fd, testLog()) | |
| 155 | + | require.ErrorIs(t, err, ErrNoComposite) | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | func TestBootOptionalExtensions(t *testing.T) { | |
| 159 | + | exts := fullPeerExts() | |
| 160 | + | delete(exts, "XFIXES") | |
| 161 | + | delete(exts, "X-Resource") | |
| 162 | + | fd, _ := startXPeer(t, exts) | |
| 163 | + | x, err := Boot(context.Background(), fd, testLog()) | |
| 164 | + | require.NoError(t, err) | |
| 165 | + | x.CloseConn() | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | func TestBootConnectFailure(t *testing.T) { | |
| 169 | + | fd, p := startXPeer(t, fullPeerExts()) | |
| 170 | + | p.hangup() | |
| 171 | + | _, err := Boot(context.Background(), fd, testLog()) | |
| 172 | + | require.Error(t, err) | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | func TestBootExistingWindows(t *testing.T) { | |
| 176 | + | x, p := bootPeer(t, func(p *xpeer) { | |
| 177 | + | p.children = []uint32{0x400001, 0x400002} | |
| 178 | + | p.attrs[0x400002] = [2]byte{1, 2} // override-redirect, viewable | |
| 179 | + | }) | |
| 180 | + | _ = p | |
| 181 | + | require.NotZero(t, x.tab.lookup(0x400001)) | |
| 182 | + | require.NotZero(t, x.tab.lookup(0x400002)) | |
| 183 | + | require.Equal(t, kindManaged, x.tab.rec(x.tab.lookup(0x400001)).kind) | |
| 184 | + | require.Equal(t, kindUnmanaged, x.tab.rec(x.tab.lookup(0x400002)).kind) | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // readUntil dispatches the socket until cond holds. | |
| 188 | + | func readUntil(t *testing.T, x *XWM, cond func() bool) { | |
| 189 | + | t.Helper() | |
| 190 | + | deadline := time.Now().Add(5 * time.Second) | |
| 191 | + | for !cond() { | |
| 192 | + | if time.Now().After(deadline) { | |
| 193 | + | t.Fatal("condition never held") | |
| 194 | + | } | |
| 195 | + | x.onReadable() | |
| 196 | + | time.Sleep(time.Millisecond) | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | func newPost() (post func(func()), runs *[]func()) { | |
| 201 | + | var q []func() | |
| 202 | + | return func(f func()) { q = append(q, f) }, &q | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | func TestAttachAnnouncesWindowsFoundAtBoot(t *testing.T) { | |
| 206 | + | x, _ := bootPeer(t, func(p *xpeer) { | |
| 207 | + | p.children = []uint32{0x400001, 0x400002} | |
| 208 | + | p.attrs[0x400002] = [2]byte{1, 2} | |
| 209 | + | }) | |
| 210 | + | h := newMockhost(t) | |
| 211 | + | src := newMockfdSource(t) | |
| 212 | + | h.EXPECT().Created(x.tab.lookup(0x400001), uint32(0x400001)).Return(ports.WindowID(7)).Once() | |
| 213 | + | post, _ := newPost() | |
| 214 | + | x.attach(src, h, post) | |
| 215 | + | require.Equal(t, ports.WindowID(7), x.tab.rec(x.tab.lookup(0x400001)).id) | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | func attached(t *testing.T) (*XWM, *xpeer, *mockhost, *mockfdSource, *[]func()) { | |
| 219 | + | t.Helper() | |
| 220 | + | x, p := bootPeer(t, nil) | |
| 221 | + | h := newMockhost(t) | |
| 222 | + | src := newMockfdSource(t) | |
| 223 | + | post, runs := newPost() | |
| 224 | + | x.attach(src, h, post) | |
| 225 | + | return x, p, h, src, runs | |
| 226 | + | } | |
| 227 | + | ||
| 228 | + | func TestCreateAndDestroyManaged(t *testing.T) { | |
| 229 | + | x, p, h, src, _ := attached(t) | |
| 230 | + | const win = 0x500001 | |
| 231 | + | h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(3)).Once() | |
| 232 | + | p.createNotify(win, false) | |
| 233 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 }) | |
| 234 | + | s := x.tab.lookup(win) | |
| 235 | + | require.Equal(t, kindManaged, x.tab.rec(s).kind) | |
| 236 | + | require.Equal(t, ports.WindowID(3), x.tab.rec(s).id) | |
| 237 | + | ||
| 238 | + | // The window is watched for property and focus changes. | |
| 239 | + | req, _ := p.waitReq(0, func(r xpeerReq) bool { return r.major == opChangeWindowAttributes && r.u32(4) == win }) | |
| 240 | + | require.EqualValues(t, xproto.EventMaskPropertyChange|xproto.EventMaskFocusChange, req.u32(12)) | |
| 241 | + | ||
| 242 | + | h.EXPECT().Dissociate(s, true).Once() | |
| 243 | + | p.destroyNotify(win) | |
| 244 | + | readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 }) | |
| 245 | + | require.Zero(t, x.tab.n) | |
| 246 | + | _ = src | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // The X server reports the XWM's own check window to it like any other | |
| 250 | + | // child of the root; it is never adopted. | |
| 251 | + | func TestOwnWindowNeverAdopted(t *testing.T) { | |
| 252 | + | x, p, _, _, _ := attached(t) // the mock fails the test on any host call | |
| 253 | + | p.createNotify(x.wmWin, false) | |
| 254 | + | const marker = 0x500003 | |
| 255 | + | p.createNotify(marker, true) | |
| 256 | + | readUntil(t, x, func() bool { return x.tab.lookup(marker) != 0 }) | |
| 257 | + | require.Zero(t, x.tab.lookup(x.wmWin)) | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | func TestOverrideRedirectNeverReachesHost(t *testing.T) { | |
| 261 | + | x, p, _, _, _ := attached(t) // the mock fails the test on any host call | |
| 262 | + | p.createNotify(0x500002, true) | |
| 263 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500002) != 0 }) | |
| 264 | + | require.Equal(t, kindUnmanaged, x.tab.rec(x.tab.lookup(0x500002)).kind) | |
| 265 | + | require.EqualValues(t, 1, x.Stats().UnmanagedSeen) | |
| 266 | + | p.destroyNotify(0x500002) | |
| 267 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500002) == 0 }) | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | func TestCloseDissociatesEverything(t *testing.T) { | |
| 271 | + | x, p, h, src, _ := attached(t) | |
| 272 | + | for _, win := range []uint32{0x500001, 0x500002} { | |
| 273 | + | h.EXPECT().Created(mock.Anything, win).Return(ports.WindowID(win)).Once() | |
| 274 | + | p.createNotify(win, false) | |
| 275 | + | } | |
| 276 | + | p.createNotify(0x500003, true) | |
| 277 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500003) != 0 }) | |
| 278 | + | h.EXPECT().Dissociate(x.tab.lookup(0x500001), true).Once() | |
| 279 | + | h.EXPECT().Dissociate(x.tab.lookup(0x500002), true).Once() | |
| 280 | + | src.EXPECT().Remove().Once() | |
| 281 | + | x.Close() | |
| 282 | + | x.Close() // idempotent | |
| 283 | + | require.True(t, x.closed) | |
| 284 | + | require.Equal(t, -1, x.conn.Fd()) | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | func TestEOFClosesXWM(t *testing.T) { | |
| 288 | + | x, p, h, src, _ := attached(t) | |
| 289 | + | h.EXPECT().Created(mock.Anything, uint32(0x500001)).Return(ports.WindowID(1)).Once() | |
| 290 | + | p.createNotify(0x500001, false) | |
| 291 | + | readUntil(t, x, func() bool { return x.tab.lookup(0x500001) != 0 }) | |
| 292 | + | h.EXPECT().Dissociate(x.tab.lookup(0x500001), true).Once() | |
| 293 | + | src.EXPECT().Remove().Once() | |
| 294 | + | p.hangup() | |
| 295 | + | readUntil(t, x, func() bool { return x.closed }) | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | func TestWakeCoalescesFlush(t *testing.T) { | |
| 299 | + | x, _, _, _, runs := attached(t) | |
| 300 | + | x.wake() | |
| 301 | + | x.wake() | |
| 302 | + | x.wake() | |
| 303 | + | require.Len(t, *runs, 1) | |
| 304 | + | (*runs)[0]() | |
| 305 | + | x.wake() | |
| 306 | + | require.Len(t, *runs, 2) | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | func TestWriteBackpressure(t *testing.T) { | |
| 310 | + | x, p, _, src, _ := attached(t) | |
| 311 | + | p.pause() | |
| 312 | + | b := x.conn.Buf() | |
| 313 | + | big := make([]byte, 64<<10) | |
| 314 | + | for i := 0; i < 64; i++ { // 4 MiB: more than any socket buffer | |
| 315 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.root), xproto.AtomWmName, xproto.AtomString, 8, uint32(len(big)), big) | |
| 316 | + | } | |
| 317 | + | src.EXPECT().SetWritable(true).Return(nil).Once() | |
| 318 | + | x.flush() | |
| 319 | + | require.True(t, x.writable) | |
| 320 | + | x.flush() // still blocked: SetWritable is not called again | |
| 321 | + | x.onWritable() | |
| 322 | + | require.True(t, x.writable) | |
| 323 | + | ||
| 324 | + | drained := false | |
| 325 | + | src.EXPECT().SetWritable(false).RunAndReturn(func(bool) error { drained = true; return nil }).Once() | |
| 326 | + | p.resume() | |
| 327 | + | deadline := time.Now().Add(10 * time.Second) | |
| 328 | + | for !drained { | |
| 329 | + | require.True(t, time.Now().Before(deadline), "never drained") | |
| 330 | + | x.onWritable() | |
| 331 | + | time.Sleep(time.Millisecond) | |
| 332 | + | } | |
| 333 | + | require.False(t, x.writable) | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | func TestFlushWithNothingQueuedSendsNothing(t *testing.T) { | |
| 337 | + | x, p, _, _, _ := attached(t) | |
| 338 | + | n := len(p.requests()) | |
| 339 | + | x.flush() | |
| 340 | + | time.Sleep(20 * time.Millisecond) | |
| 341 | + | require.Len(t, p.requests(), n, "nothing queued, nothing sent") | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | // The XWM never blocks after Boot: NewXID, Sync and InternAtoms are startup | |
| 345 | + | // only, so they may appear in production code only inside boot(). | |
| 346 | + | func TestNoBlockingCallsOutsideBoot(t *testing.T) { | |
| 347 | + | files, err := os.ReadDir(".") | |
| 348 | + | require.NoError(t, err) | |
| 349 | + | for _, f := range files { | |
| 350 | + | name := f.Name() | |
| 351 | + | if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { | |
| 352 | + | continue | |
| 353 | + | } | |
| 354 | + | src, err := os.ReadFile(name) | |
| 355 | + | require.NoError(t, err) | |
| 356 | + | text := string(src) | |
| 357 | + | lo, hi := 0, 0 | |
| 358 | + | if name == "xwm.go" { | |
| 359 | + | lo = strings.Index(text, "func (x *XWM) boot() error") | |
| 360 | + | hi = strings.Index(text, "// Attach registers") | |
| 361 | + | require.Greater(t, hi, lo) | |
| 362 | + | } | |
| 363 | + | for _, call := range []string{".NewXID(", ".Sync(", ".InternAtoms("} { | |
| 364 | + | for _, at := range allIndex(text, call) { | |
| 365 | + | require.True(t, name == "xwm.go" && at > lo && at < hi, "%s: %s outside boot", name, call) | |
| 366 | + | } | |
| 367 | + | } | |
| 368 | + | } | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | func allIndex(s, sub string) []int { | |
| 372 | + | var out []int | |
| 373 | + | for off := 0; ; { | |
| 374 | + | i := strings.Index(s[off:], sub) | |
| 375 | + | if i < 0 { | |
| 376 | + | return out | |
| 377 | + | } | |
| 378 | + | out = append(out, off+i) | |
| 379 | + | off += i + len(sub) | |
| 380 | + | } | |
| 381 | + | } | |
A internal/adapters/xwayland/builtin.go +216 -0
| @@ -0,0 +1,216 @@ | |||
| 1 | + | package xwayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "fmt" | |
| 7 | + | "os" | |
| 8 | + | "os/exec" | |
| 9 | + | "strconv" | |
| 10 | + | "strings" | |
| 11 | + | "syscall" | |
| 12 | + | "time" | |
| 13 | + | ||
| 14 | + | "golang.org/x/sys/unix" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | type builtinStarter struct{} | |
| 18 | + | ||
| 19 | + | func (builtinStarter) start(ctx context.Context, d *Display, stderr *logWriter) error { | |
| 20 | + | // Files own all setup descriptors until Start succeeds and Attach takes | |
| 21 | + | // the Wayland socket. This also unwinds partial setup failures. | |
| 22 | + | var files []*os.File | |
| 23 | + | defer func() { | |
| 24 | + | for _, f := range files { | |
| 25 | + | if f != nil { | |
| 26 | + | f.Close() | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | }() | |
| 30 | + | pair := func(name string) (int, error) { | |
| 31 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 32 | + | if err != nil { | |
| 33 | + | return 0, err | |
| 34 | + | } | |
| 35 | + | i := len(files) | |
| 36 | + | files = append(files, os.NewFile(uintptr(fds[0]), name+" parent"), os.NewFile(uintptr(fds[1]), name+" child")) | |
| 37 | + | return i, nil | |
| 38 | + | } | |
| 39 | + | wl, err := pair("wayland") | |
| 40 | + | if err != nil { | |
| 41 | + | return err | |
| 42 | + | } | |
| 43 | + | wm, err := pair("wm") | |
| 44 | + | if err != nil { | |
| 45 | + | return err | |
| 46 | + | } | |
| 47 | + | var pipe [2]int | |
| 48 | + | if err := unix.Pipe2(pipe[:], unix.O_CLOEXEC); err != nil { | |
| 49 | + | return err | |
| 50 | + | } | |
| 51 | + | rd := os.NewFile(uintptr(pipe[0]), "displayfd read") | |
| 52 | + | wr := os.NewFile(uintptr(pipe[1]), "displayfd write") | |
| 53 | + | files = append(files, rd, wr) | |
| 54 | + | args := []string{d.Name(), "-rootless"} | |
| 55 | + | for i := range d.sockets { | |
| 56 | + | args = append(args, "-listenfd", strconv.Itoa(3+i)) | |
| 57 | + | } | |
| 58 | + | wlChild := 3 + len(d.sockets) | |
| 59 | + | args = append(args, "-wm", strconv.Itoa(wlChild+1), "-displayfd", strconv.Itoa(wlChild+2)) | |
| 60 | + | cmd := exec.Command(d.opts.Binary, args...) | |
| 61 | + | for _, e := range d.opts.Env { | |
| 62 | + | if !strings.HasPrefix(e, "WAYLAND_DISPLAY=") && !strings.HasPrefix(e, "WAYLAND_SOCKET=") { | |
| 63 | + | cmd.Env = append(cmd.Env, e) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | cmd.Env = append(cmd.Env, "WAYLAND_SOCKET="+strconv.Itoa(wlChild)) | |
| 67 | + | cmd.ExtraFiles = append(append([]*os.File(nil), d.sockets...), files[wl+1], files[wm+1], wr) | |
| 68 | + | cmd.Stderr = stderr | |
| 69 | + | cmd.WaitDelay = 2 * time.Second | |
| 70 | + | cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} | |
| 71 | + | if err := cmd.Start(); err != nil { | |
| 72 | + | return err | |
| 73 | + | } | |
| 74 | + | files[wl+1].Close() | |
| 75 | + | files[wm+1].Close() | |
| 76 | + | wr.Close() | |
| 77 | + | d.opts.Log.Info().Str("display", d.Name()).Int("pid", cmd.Process.Pid).Msg("Xwayland started") | |
| 78 | + | // Detach the Wayland descriptor from os.File ownership before handing it | |
| 79 | + | // to the display (including its finalizers). | |
| 80 | + | if d.opts.Attach != nil { | |
| 81 | + | // Dup transfers an independent descriptor; closing the File wrapper below | |
| 82 | + | // cannot race with the display's ownership of the transferred descriptor. | |
| 83 | + | var wlFD int | |
| 84 | + | wlFD, err = unix.FcntlInt(uintptr(files[wl].Fd()), unix.F_DUPFD_CLOEXEC, 0) | |
| 85 | + | if err == nil { | |
| 86 | + | d.opts.Attach(wlFD) | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | files[wl].Close() | |
| 90 | + | done := make(chan error, 1) | |
| 91 | + | procGone := make(chan struct{}) | |
| 92 | + | go func() { done <- cmd.Wait(); close(procGone) }() | |
| 93 | + | if err != nil { | |
| 94 | + | stopGroup(cmd, done) | |
| 95 | + | return err | |
| 96 | + | } | |
| 97 | + | exited, readyErr := waitReady(ctx, pipe[0], done) | |
| 98 | + | if readyErr != nil { | |
| 99 | + | err := readyErr | |
| 100 | + | if errors.Is(err, errReadinessTimeout) { | |
| 101 | + | d.opts.Log.Warn().Str("display", d.Name()).Msg("Xwayland readiness timed out") | |
| 102 | + | } | |
| 103 | + | // Join Wait unless readiness already observed process exit. | |
| 104 | + | if !exited { | |
| 105 | + | stopGroup(cmd, done) | |
| 106 | + | } | |
| 107 | + | return err | |
| 108 | + | } | |
| 109 | + | rd.Close() | |
| 110 | + | d.opts.Log.Info().Str("display", d.Name()).Msg("Xwayland ready") | |
| 111 | + | if d.opts.AttachWM != nil { | |
| 112 | + | // The WM socket is bootstrapped only now: Xwayland serves it after its | |
| 113 | + | // Wayland round trips, which the display goroutine must be free to answer. | |
| 114 | + | wmFD, err := unix.FcntlInt(uintptr(files[wm].Fd()), unix.F_DUPFD_CLOEXEC, 0) | |
| 115 | + | if err != nil { | |
| 116 | + | stopGroup(cmd, done) | |
| 117 | + | return err | |
| 118 | + | } | |
| 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 | |
| 134 | + | if stop != nil { | |
| 135 | + | // Runs before start returns so the restart loop never overlaps two XWMs. | |
| 136 | + | defer stop() | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | select { | |
| 140 | + | case err := <-done: | |
| 141 | + | return err | |
| 142 | + | case <-ctx.Done(): | |
| 143 | + | stopGroup(cmd, done) | |
| 144 | + | return ctx.Err() | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | var errReadinessTimeout = errors.New("xwayland readiness timed out") | |
| 149 | + | ||
| 150 | + | // waitReady uses bounded poll waits rather than a pipe reader goroutine. | |
| 151 | + | // The result reports whether cmd.Wait was already joined. | |
| 152 | + | func waitReady(ctx context.Context, fd int, done <-chan error) (bool, error) { | |
| 153 | + | deadline := time.Now().Add(10 * time.Second) | |
| 154 | + | var buf [32]byte | |
| 155 | + | n := 0 | |
| 156 | + | for { | |
| 157 | + | select { | |
| 158 | + | case err := <-done: | |
| 159 | + | if err == nil { | |
| 160 | + | err = errors.New("xwayland exited before readiness") | |
| 161 | + | } | |
| 162 | + | return true, err | |
| 163 | + | default: | |
| 164 | + | } | |
| 165 | + | if ctx.Err() != nil { | |
| 166 | + | return false, ctx.Err() | |
| 167 | + | } | |
| 168 | + | if time.Now().After(deadline) { | |
| 169 | + | return false, errReadinessTimeout | |
| 170 | + | } | |
| 171 | + | // A closed pipe also covers process exit, including exec's stderr delay. | |
| 172 | + | p := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}} | |
| 173 | + | _, err := unix.Poll(p, 50) | |
| 174 | + | if errors.Is(err, unix.EINTR) { | |
| 175 | + | continue | |
| 176 | + | } | |
| 177 | + | if err != nil { | |
| 178 | + | return false, err | |
| 179 | + | } | |
| 180 | + | if p[0].Revents == 0 { | |
| 181 | + | continue | |
| 182 | + | } | |
| 183 | + | if p[0].Revents&unix.POLLIN != 0 { | |
| 184 | + | count, err := unix.Read(fd, buf[n:]) | |
| 185 | + | if errors.Is(err, unix.EINTR) { | |
| 186 | + | continue | |
| 187 | + | } | |
| 188 | + | if err != nil { | |
| 189 | + | return false, err | |
| 190 | + | } | |
| 191 | + | n += count | |
| 192 | + | if n > 0 && buf[n-1] == '\n' { | |
| 193 | + | if _, err := strconv.Atoi(strings.TrimSpace(string(buf[:n]))); err != nil { | |
| 194 | + | return false, fmt.Errorf("xwayland displayfd: %w", err) | |
| 195 | + | } | |
| 196 | + | return false, nil | |
| 197 | + | } | |
| 198 | + | if n == len(buf) { | |
| 199 | + | return false, errors.New("invalid Xwayland displayfd response") | |
| 200 | + | } | |
| 201 | + | } | |
| 202 | + | if p[0].Revents&(unix.POLLHUP|unix.POLLERR|unix.POLLNVAL) != 0 { | |
| 203 | + | return false, errors.New("xwayland exited before readiness") | |
| 204 | + | } | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | func stopGroup(cmd *exec.Cmd, done <-chan error) { | |
| 209 | + | _ = unix.Kill(-cmd.Process.Pid, unix.SIGTERM) | |
| 210 | + | select { | |
| 211 | + | case <-done: | |
| 212 | + | case <-time.After(2 * time.Second): | |
| 213 | + | _ = unix.Kill(-cmd.Process.Pid, unix.SIGKILL) | |
| 214 | + | <-done | |
| 215 | + | } | |
| 216 | + | } | |
A internal/adapters/xwayland/starter_mock_test.go +110 -0
| @@ -0,0 +1,110 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package xwayland | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | "context" | |
| 9 | + | ||
| 10 | + | mock "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // newMockstarter creates a new instance of mockstarter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 14 | + | // The first argument is typically a *testing.T value. | |
| 15 | + | func newMockstarter(t interface { | |
| 16 | + | mock.TestingT | |
| 17 | + | Cleanup(func()) | |
| 18 | + | }) *mockstarter { | |
| 19 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 20 | + | helper.Helper() | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | mock := &mockstarter{} | |
| 24 | + | mock.Mock.Test(t) | |
| 25 | + | ||
| 26 | + | t.Cleanup(func() { | |
| 27 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 28 | + | helper.Helper() | |
| 29 | + | } | |
| 30 | + | mock.AssertExpectations(t) | |
| 31 | + | }) | |
| 32 | + | ||
| 33 | + | return mock | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // mockstarter is an autogenerated mock type for the starter type | |
| 37 | + | type mockstarter struct { | |
| 38 | + | mock.Mock | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | type mockstarter_Expecter struct { | |
| 42 | + | mock *mock.Mock | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (_m *mockstarter) EXPECT() *mockstarter_Expecter { | |
| 46 | + | return &mockstarter_Expecter{mock: &_m.Mock} | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // start provides a mock function for the type mockstarter | |
| 50 | + | func (_mock *mockstarter) start(ctx context.Context, d *Display, stderr *logWriter) error { | |
| 51 | + | ret := _mock.Called(ctx, d, stderr) | |
| 52 | + | ||
| 53 | + | if len(ret) == 0 { | |
| 54 | + | panic("no return value specified for start") | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | var r0 error | |
| 58 | + | if returnFunc, ok := ret.Get(0).(func(context.Context, *Display, *logWriter) error); ok { | |
| 59 | + | r0 = returnFunc(ctx, d, stderr) | |
| 60 | + | } else { | |
| 61 | + | r0 = ret.Error(0) | |
| 62 | + | } | |
| 63 | + | return r0 | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // mockstarter_start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'start' | |
| 67 | + | type mockstarter_start_Call struct { | |
| 68 | + | *mock.Call | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // start is a helper method to define mock.On call | |
| 72 | + | // - ctx context.Context | |
| 73 | + | // - d *Display | |
| 74 | + | // - stderr *logWriter | |
| 75 | + | func (_e *mockstarter_Expecter) start(ctx any, d any, stderr any) *mockstarter_start_Call { | |
| 76 | + | return &mockstarter_start_Call{Call: _e.mock.On("start", ctx, d, stderr)} | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func (_c *mockstarter_start_Call) Run(run func(ctx context.Context, d *Display, stderr *logWriter)) *mockstarter_start_Call { | |
| 80 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 81 | + | var arg0 context.Context | |
| 82 | + | if args[0] != nil { | |
| 83 | + | arg0 = args[0].(context.Context) | |
| 84 | + | } | |
| 85 | + | var arg1 *Display | |
| 86 | + | if args[1] != nil { | |
| 87 | + | arg1 = args[1].(*Display) | |
| 88 | + | } | |
| 89 | + | var arg2 *logWriter | |
| 90 | + | if args[2] != nil { | |
| 91 | + | arg2 = args[2].(*logWriter) | |
| 92 | + | } | |
| 93 | + | run( | |
| 94 | + | arg0, | |
| 95 | + | arg1, | |
| 96 | + | arg2, | |
| 97 | + | ) | |
| 98 | + | }) | |
| 99 | + | return _c | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func (_c *mockstarter_start_Call) Return(err error) *mockstarter_start_Call { | |
| 103 | + | _c.Call.Return(err) | |
| 104 | + | return _c | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | func (_c *mockstarter_start_Call) RunAndReturn(run func(ctx context.Context, d *Display, stderr *logWriter) error) *mockstarter_start_Call { | |
| 108 | + | _c.Call.Return(run) | |
| 109 | + | return _c | |
| 110 | + | } | |
M internal/adapters/xwayland/xwayland.go +46 -18
| @@ -1,6 +1,5 @@ | |||
| 1 | - | // Package xwayland runs X11 clients through xwayland-satellite, started on | |
| 2 | - | // demand: neferwl owns the X11 display sockets and starts the satellite | |
| 3 | - | // when the first X11 client connects, and again after it exits. | |
| 1 | + | // Package xwayland starts an X11 backend on demand. NeferWL owns the display | |
| 2 | + | // sockets and starts the selected backend when the first client connects. | |
| 4 | 3 | package xwayland | |
| 5 | 4 | ||
| 6 | 5 | import ( | |
| @@ -17,15 +16,26 @@ import ( | |||
| 17 | 16 | "syscall" | |
| 18 | 17 | "time" | |
| 19 | 18 | ||
| 19 | + | "github.com/bnema/neferwl/internal/ports" | |
| 20 | 20 | "github.com/bnema/zerowrap" | |
| 21 | 21 | "golang.org/x/sys/unix" | |
| 22 | 22 | ) | |
| 23 | 23 | ||
| 24 | 24 | // Options configures the X11 display. | |
| 25 | 25 | type Options struct { | |
| 26 | - | // Binary is the xwayland-satellite executable. | |
| 26 | + | Backend ports.XwaylandBackend | |
| 27 | + | // Attach takes ownership of the Wayland descriptor right after the backend | |
| 28 | + | // starts. | |
| 29 | + | Attach func(wlFD int) | |
| 30 | + | // AttachWM takes ownership of the window manager descriptor once the | |
| 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()) | |
| 36 | + | // Binary is the selected backend executable. | |
| 27 | 37 | Binary string | |
| 28 | - | // Env is the satellite's environment; it must hold WAYLAND_DISPLAY. | |
| 38 | + | // Env is the backend environment; satellite requires WAYLAND_DISPLAY. | |
| 29 | 39 | Env []string | |
| 30 | 40 | // Dir holds the X11 sockets and TmpDir the lock files: /tmp/.X11-unix | |
| 31 | 41 | // and /tmp outside tests. | |
| @@ -35,8 +45,9 @@ type Options struct { | |||
| 35 | 45 | Log zerowrap.Logger | |
| 36 | 46 | } | |
| 37 | 47 | ||
| 38 | - | // Display is an X11 display served by xwayland-satellite. | |
| 48 | + | // Display is an on-demand X11 display. | |
| 39 | 49 | type Display struct { | |
| 50 | + | starter starter | |
| 40 | 51 | opts Options | |
| 41 | 52 | n int | |
| 42 | 53 | sockets []*os.File // unix socket, then the abstract one | |
| @@ -81,7 +92,10 @@ func Open(opts Options) (*Display, error) { | |||
| 81 | 92 | _ = os.Remove(lock) | |
| 82 | 93 | return nil, fmt.Errorf("write X11 lock: %w", err) | |
| 83 | 94 | } | |
| 84 | - | d := &Display{opts: opts, n: n, lock: lock, socket: filepath.Join(opts.Dir, fmt.Sprintf("X%d", n))} | |
| 95 | + | d := &Display{starter: satelliteStarter{}, opts: opts, n: n, lock: lock, socket: filepath.Join(opts.Dir, fmt.Sprintf("X%d", n))} | |
| 96 | + | if opts.Backend == ports.XwaylandBuiltin { | |
| 97 | + | d.starter = builtinStarter{} | |
| 98 | + | } | |
| 85 | 99 | if err := d.listen(); err != nil { | |
| 86 | 100 | last = err | |
| 87 | 101 | d.Close() | |
| @@ -212,15 +226,15 @@ func (d *Display) Run(ctx context.Context) error { | |||
| 212 | 226 | return err | |
| 213 | 227 | } | |
| 214 | 228 | start := time.Now() | |
| 215 | - | stderr := &logWriter{log: d.opts.Log} | |
| 216 | - | err := d.serve(ctx, stderr) | |
| 229 | + | stderr := &logWriter{log: d.opts.Log, backend: d.backendName()} | |
| 230 | + | err := d.starter.start(ctx, d, stderr) | |
| 217 | 231 | if ctx.Err() != nil { | |
| 218 | 232 | return nil | |
| 219 | 233 | } | |
| 220 | 234 | if err != nil { | |
| 221 | - | d.opts.Log.Warn().Err(err).Strs("stderr", stderr.tail()).Str("display", d.Name()).Msg("xwayland-satellite exited") | |
| 235 | + | d.opts.Log.Warn().Err(err).Strs("stderr", stderr.tail()).Str("display", d.Name()).Msg(d.backendName() + " exited") | |
| 222 | 236 | } else { | |
| 223 | - | d.opts.Log.Info().Str("display", d.Name()).Msg("xwayland-satellite exited") | |
| 237 | + | d.opts.Log.Info().Str("display", d.Name()).Msg(d.backendName() + " exited") | |
| 224 | 238 | } | |
| 225 | 239 | // Clients the satellite never accepted would wake us at once: a | |
| 226 | 240 | // satellite that cannot start must not respawn in a loop. | |
| @@ -261,8 +275,14 @@ func (d *Display) waitClient(ctx context.Context, wake int) error { | |||
| 261 | 275 | return ctx.Err() | |
| 262 | 276 | } | |
| 263 | 277 | ||
| 264 | - | // serve runs the satellite on the sockets until it exits or ctx ends. | |
| 265 | - | func (d *Display) serve(ctx context.Context, stderr *logWriter) error { | |
| 278 | + | // starter runs a backend on the reserved sockets until it exits or ctx ends. | |
| 279 | + | type starter interface { | |
| 280 | + | start(ctx context.Context, d *Display, stderr *logWriter) error | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | type satelliteStarter struct{} | |
| 284 | + | ||
| 285 | + | func (satelliteStarter) start(ctx context.Context, d *Display, stderr *logWriter) error { | |
| 266 | 286 | args := []string{d.Name()} | |
| 267 | 287 | for i := range d.sockets { | |
| 268 | 288 | args = append(args, "-listenfd", strconv.Itoa(3+i)) | |
| @@ -318,10 +338,11 @@ func (d *Display) dropPending() { | |||
| 318 | 338 | // logWriter logs the satellite's stderr line by line and keeps the last | |
| 319 | 339 | // lines for the exit log; exec copies it from a pipe on its own goroutine. | |
| 320 | 340 | type logWriter struct { | |
| 321 | - | mu sync.Mutex | |
| 322 | - | log zerowrap.Logger | |
| 323 | - | line []byte | |
| 324 | - | last []string | |
| 341 | + | backend string | |
| 342 | + | mu sync.Mutex | |
| 343 | + | log zerowrap.Logger | |
| 344 | + | line []byte | |
| 345 | + | last []string | |
| 325 | 346 | } | |
| 326 | 347 | ||
| 327 | 348 | // tail is the last lines written. | |
| @@ -342,7 +363,7 @@ func (w *logWriter) Write(p []byte) (int, error) { | |||
| 342 | 363 | } | |
| 343 | 364 | if i > 0 { | |
| 344 | 365 | line := string(w.line[:i]) | |
| 345 | - | w.log.Debug().Str("line", line).Msg("xwayland-satellite") | |
| 366 | + | w.log.Debug().Str("line", line).Msg(w.backend) | |
| 346 | 367 | if w.last = append(w.last, line); len(w.last) > 5 { | |
| 347 | 368 | w.last = w.last[1:] | |
| 348 | 369 | } | |
| @@ -354,3 +375,10 @@ func (w *logWriter) Write(p []byte) (int, error) { | |||
| 354 | 375 | } | |
| 355 | 376 | return len(p), nil | |
| 356 | 377 | } | |
| 378 | + | ||
| 379 | + | func (d *Display) backendName() string { | |
| 380 | + | if d.opts.Backend == ports.XwaylandBuiltin { | |
| 381 | + | return "Xwayland" | |
| 382 | + | } | |
| 383 | + | return "xwayland-satellite" | |
| 384 | + | } | |
M internal/adapters/xwayland/xwayland_test.go +207 -0
| @@ -11,6 +11,8 @@ import ( | |||
| 11 | 11 | "time" | |
| 12 | 12 | ||
| 13 | 13 | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 14 | + | "github.com/bnema/neferwl/internal/ports" | |
| 15 | + | "github.com/stretchr/testify/mock" | |
| 14 | 16 | "golang.org/x/sys/unix" | |
| 15 | 17 | ) | |
| 16 | 18 | ||
| @@ -270,3 +272,208 @@ func TestRunKillsSatelliteGroup(t *testing.T) { | |||
| 270 | 272 | } | |
| 271 | 273 | } | |
| 272 | 274 | } | |
| 275 | + | ||
| 276 | + | func TestStarterSelection(t *testing.T) { | |
| 277 | + | for _, backend := range []ports.XwaylandBackend{ports.XwaylandSatellite, ports.XwaylandBuiltin} { | |
| 278 | + | opts := testOptions(t, "true") | |
| 279 | + | opts.Backend = backend | |
| 280 | + | d, err := Open(opts) | |
| 281 | + | if err != nil { | |
| 282 | + | t.Fatal(err) | |
| 283 | + | } | |
| 284 | + | defer d.Close() | |
| 285 | + | switch d.starter.(type) { | |
| 286 | + | case satelliteStarter: | |
| 287 | + | if backend != ports.XwaylandSatellite { | |
| 288 | + | t.Fatal("satellite selected") | |
| 289 | + | } | |
| 290 | + | case builtinStarter: | |
| 291 | + | if backend != ports.XwaylandBuiltin { | |
| 292 | + | t.Fatal("builtin selected") | |
| 293 | + | } | |
| 294 | + | default: | |
| 295 | + | t.Fatalf("unexpected starter %T", d.starter) | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | func TestRunCallsStarter(t *testing.T) { | |
| 301 | + | d, err := Open(testOptions(t, "unused")) | |
| 302 | + | if err != nil { | |
| 303 | + | t.Fatal(err) | |
| 304 | + | } | |
| 305 | + | defer d.Close() | |
| 306 | + | ctx, cancel := context.WithCancel(t.Context()) | |
| 307 | + | defer cancel() | |
| 308 | + | m := newMockstarter(t) | |
| 309 | + | d.starter = m | |
| 310 | + | m.EXPECT().start(ctx, d, mock.Anything).RunAndReturn(func(_ context.Context, got *Display, stderr *logWriter) error { | |
| 311 | + | if got != d || stderr == nil { | |
| 312 | + | t.Error("starter arguments") | |
| 313 | + | } | |
| 314 | + | cancel() | |
| 315 | + | return ctx.Err() | |
| 316 | + | }).Once() | |
| 317 | + | done := make(chan error, 1) | |
| 318 | + | go func() { done <- d.Run(ctx) }() | |
| 319 | + | unix.Close(dial(t, d.socket)) | |
| 320 | + | select { | |
| 321 | + | case err := <-done: | |
| 322 | + | if err != nil { | |
| 323 | + | t.Fatal(err) | |
| 324 | + | } | |
| 325 | + | case <-time.After(3 * time.Second): | |
| 326 | + | t.Fatal("Run did not stop") | |
| 327 | + | } | |
| 328 | + | } | |
| 329 | + | ||
| 330 | + | func TestWaitReady(t *testing.T) { | |
| 331 | + | for _, tt := range []struct { | |
| 332 | + | name, data string | |
| 333 | + | wantErr bool | |
| 334 | + | }{ | |
| 335 | + | {"ready", "12\n", false}, {"invalid", "bad\n", true}, {"closed", "", true}, | |
| 336 | + | } { | |
| 337 | + | t.Run(tt.name, func(t *testing.T) { | |
| 338 | + | var p [2]int | |
| 339 | + | if err := unix.Pipe2(p[:], unix.O_CLOEXEC); err != nil { | |
| 340 | + | t.Fatal(err) | |
| 341 | + | } | |
| 342 | + | defer unix.Close(p[0]) | |
| 343 | + | if tt.data != "" { | |
| 344 | + | if _, err := unix.Write(p[1], []byte(tt.data)); err != nil { | |
| 345 | + | t.Fatal(err) | |
| 346 | + | } | |
| 347 | + | } | |
| 348 | + | unix.Close(p[1]) | |
| 349 | + | joined, err := waitReady(t.Context(), p[0], make(chan error)) | |
| 350 | + | if joined || (err != nil) != tt.wantErr { | |
| 351 | + | t.Fatalf("joined=%v err=%v", joined, err) | |
| 352 | + | } | |
| 353 | + | }) | |
| 354 | + | } | |
| 355 | + | t.Run("exit", func(t *testing.T) { | |
| 356 | + | done := make(chan error, 1) | |
| 357 | + | done <- errors.New("exit") | |
| 358 | + | joined, err := waitReady(t.Context(), -1, done) | |
| 359 | + | if !joined || err == nil { | |
| 360 | + | t.Fatal(joined, err) | |
| 361 | + | } | |
| 362 | + | }) | |
| 363 | + | t.Run("cancel", func(t *testing.T) { | |
| 364 | + | ctx, cancel := context.WithCancel(t.Context()) | |
| 365 | + | cancel() | |
| 366 | + | joined, err := waitReady(ctx, -1, make(chan error)) | |
| 367 | + | if joined || !errors.Is(err, context.Canceled) { | |
| 368 | + | t.Fatal(joined, err) | |
| 369 | + | } | |
| 370 | + | }) | |
| 371 | + | } | |
| 372 | + | ||
| 373 | + | // readyScript stands in for Xwayland: after a short delay it creates a marker | |
| 374 | + | // file and reports readiness on the -displayfd descriptor when ready is true, | |
| 375 | + | // then runs tail. | |
| 376 | + | func readyScript(t *testing.T, dir string, ready bool, tail string) (bin, marker string) { | |
| 377 | + | t.Helper() | |
| 378 | + | bin, marker = filepath.Join(dir, "xwayland"), filepath.Join(dir, "ready") | |
| 379 | + | script := "#!/bin/sh\nwhile [ $# -gt 0 ]; do [ \"$1\" = -displayfd ] && fd=$2; shift; done\n" + | |
| 380 | + | "sleep 0.3\n" | |
| 381 | + | if ready { | |
| 382 | + | script += ": > " + marker + "\neval \"echo 1 >&$fd\"\n" | |
| 383 | + | } | |
| 384 | + | script += tail + "\n" | |
| 385 | + | if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { | |
| 386 | + | t.Fatal(err) | |
| 387 | + | } | |
| 388 | + | return bin, marker | |
| 389 | + | } | |
| 390 | + | ||
| 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() | |
| 393 | + | opts := testOptions(t, "") | |
| 394 | + | opts.Backend = ports.XwaylandBuiltin | |
| 395 | + | var marker string | |
| 396 | + | opts.Binary, marker = readyScript(t, opts.TmpDir, ready, tail) | |
| 397 | + | exists := func() bool { _, err := os.Stat(marker); return err == nil } | |
| 398 | + | opts.Attach = func(wlFD int) { | |
| 399 | + | if exists() { | |
| 400 | + | t.Error("Attach ran after readiness") | |
| 401 | + | } | |
| 402 | + | events = append(events, "attach") | |
| 403 | + | unix.Close(wlFD) | |
| 404 | + | } | |
| 405 | + | opts.AttachWM = func(ctx context.Context, wmFD int) func() { | |
| 406 | + | if !exists() { | |
| 407 | + | t.Error("AttachWM ran before readiness") | |
| 408 | + | } | |
| 409 | + | events = append(events, "attachWM") | |
| 410 | + | attachWM(ctx, wmFD) | |
| 411 | + | return func() { events = append(events, "stop") } | |
| 412 | + | } | |
| 413 | + | d, err := Open(opts) | |
| 414 | + | if err != nil { | |
| 415 | + | t.Fatal(err) | |
| 416 | + | } | |
| 417 | + | defer d.Close() | |
| 418 | + | _ = d.starter.start(ctx, d, &logWriter{log: opts.Log, backend: d.backendName()}) | |
| 419 | + | return append(events, "returned") | |
| 420 | + | } | |
| 421 | + | ||
| 422 | + | // AttachWM runs only after -displayfd readiness; stop runs before start | |
| 423 | + | // returns, whether the process exits or the context ends. | |
| 424 | + | func TestBuiltinAttachWMAfterReadiness(t *testing.T) { | |
| 425 | + | for name, tc := range map[string]struct { | |
| 426 | + | tail string | |
| 427 | + | cancel bool | |
| 428 | + | }{"process exit": {tail: "exit 0"}, "context end": {tail: "exec sleep 30", cancel: true}} { | |
| 429 | + | t.Run(name, func(t *testing.T) { | |
| 430 | + | ctx, cancel := context.WithCancel(t.Context()) | |
| 431 | + | defer cancel() | |
| 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 { | |
| 434 | + | t.Errorf("wmFD not open: %v", err) | |
| 435 | + | } | |
| 436 | + | unix.Close(wmFD) | |
| 437 | + | if tc.cancel { | |
| 438 | + | go func() { time.Sleep(200 * time.Millisecond); cancel() }() | |
| 439 | + | } | |
| 440 | + | }) | |
| 441 | + | if got := strings.Join(events, ","); got != "attach,attachWM,stop,returned" { | |
| 442 | + | t.Fatalf("events %s", got) | |
| 443 | + | } | |
| 444 | + | }) | |
| 445 | + | } | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | // A process that exits before reporting readiness never reaches AttachWM. | |
| 449 | + | func TestBuiltinNoAttachWMWithoutReadiness(t *testing.T) { | |
| 450 | + | events := runBuiltin(t, t.Context(), false, "exit 1", func(context.Context, int) {}) | |
| 451 | + | if got := strings.Join(events, ","); got != "attach,returned" { | |
| 452 | + | t.Fatalf("events %s", got) | |
| 453 | + | } | |
| 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 +13 -6
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "context" | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "io" | |
| 8 | + | "os/exec" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/neferwl/internal/adapters/logging" | |
| @@ -188,18 +189,24 @@ func logScenes(log zerowrap.Logger, set []ports.Scene) { | |||
| 188 | 189 | } | |
| 189 | 190 | } | |
| 190 | 191 | ||
| 191 | - | // openXwayland reserves an X11 display for xwayland-satellite, or returns | |
| 192 | + | // openXwayland reserves an X11 display for the selected backend, or returns | |
| 192 | 193 | // nil when X11 is off or unavailable: the session runs without it. | |
| 193 | - | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger) *xwayland.Display { | |
| 194 | - | bin := opts.Config.Xwayland | |
| 195 | - | if bin == "" || opts.NoXwayland { | |
| 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 | |
| 196 | + | bin := cfg.Binary | |
| 197 | + | if cfg.Backend == ports.XwaylandOff || opts.NoXwayland { | |
| 196 | 198 | return nil | |
| 197 | 199 | } | |
| 198 | - | if !xwayland.Supported(ctx, bin) { | |
| 200 | + | if cfg.Backend == ports.XwaylandBuiltin { | |
| 201 | + | if _, err := exec.LookPath(bin); err != nil { | |
| 202 | + | log.Warn().Err(err).Str("binary", bin).Msg("Xwayland not found: X11 apps disabled") | |
| 203 | + | return nil | |
| 204 | + | } | |
| 205 | + | } else if !xwayland.Supported(ctx, bin) { | |
| 199 | 206 | log.Warn().Str("binary", bin).Msg("xwayland-satellite 0.7 or later not found: X11 apps disabled") | |
| 200 | 207 | return nil | |
| 201 | 208 | } | |
| 202 | - | d, err := xwayland.Open(xwayland.Options{Binary: bin, Env: env, Dir: "/tmp/.X11-unix", TmpDir: "/tmp", Abstract: true, Log: log}) | |
| 209 | + | d, err := xwayland.Open(xwayland.Options{Backend: cfg.Backend, Attach: attach, AttachWM: attachWM, Binary: bin, Env: env, Dir: "/tmp/.X11-unix", TmpDir: "/tmp", Abstract: true, Log: log}) | |
| 203 | 210 | if err != nil { | |
| 204 | 211 | log.Warn().Err(err).Msg("X11 display disabled") | |
| 205 | 212 | return nil | |
M internal/app/session.go +1 -1
| @@ -152,7 +152,7 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error) | |||
| 152 | 152 | s.statePath = statePath | |
| 153 | 153 | s.childEnv = append(s.childEnv, statefile.Env+"="+statePath) | |
| 154 | 154 | } | |
| 155 | - | if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland")); s.xdisplay != nil { | |
| 155 | + | if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland"), s.server.AttachXwayland, s.server.AttachXWM); s.xdisplay != nil { | |
| 156 | 156 | s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name()) | |
| 157 | 157 | } | |
| 158 | 158 | s.child = launcher.New(s.childEnv, s.security, logging.For(ctx, "launcher")) | |
A internal/app/testdata/x11probe/main.go +141 -0
| @@ -0,0 +1,141 @@ | |||
| 1 | + | // Command x11probe is a minimal X11 client for the Xwayland end-to-end test. | |
| 2 | + | // | |
| 3 | + | // It opens one window filled with solid green, prints "mapped WxH" after the | |
| 4 | + | // first ConfigureNotify and "focus" on each FocusIn, and exits 0 when the | |
| 5 | + | // window manager sends WM_DELETE_WINDOW. | |
| 6 | + | package main | |
| 7 | + | ||
| 8 | + | import ( | |
| 9 | + | "encoding/binary" | |
| 10 | + | "flag" | |
| 11 | + | "fmt" | |
| 12 | + | "os" | |
| 13 | + | "strconv" | |
| 14 | + | "strings" | |
| 15 | + | "time" | |
| 16 | + | ||
| 17 | + | "golang.org/x/sys/unix" | |
| 18 | + | ||
| 19 | + | "bnema.dev/xturbo" | |
| 20 | + | "bnema.dev/xturbo/xproto" | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | const green = 0x00ff00 | |
| 24 | + | ||
| 25 | + | func main() { | |
| 26 | + | if err := run(); err != nil { | |
| 27 | + | fmt.Fprintln(os.Stderr, "x11probe:", err) | |
| 28 | + | os.Exit(1) | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func run() error { | |
| 33 | + | class := flag.String("class", "X11Probe", "WM_CLASS class (the instance is x11probe)") | |
| 34 | + | title := flag.String("title", "probe", "_NET_WM_NAME") | |
| 35 | + | size := flag.String("size", "300x200", "window size WxH") | |
| 36 | + | sleep := flag.Duration("sleep", 0, "wait before mapping the window") | |
| 37 | + | flag.Parse() | |
| 38 | + | w, h, ok := parseSize(*size) | |
| 39 | + | if !ok { | |
| 40 | + | return fmt.Errorf("bad -size %q", *size) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | c, err := xturbo.Dial("") // $DISPLAY | |
| 44 | + | if err != nil { | |
| 45 | + | return err | |
| 46 | + | } | |
| 47 | + | defer c.Close() | |
| 48 | + | ||
| 49 | + | var atoms [4]uint32 | |
| 50 | + | if err := c.InternAtoms([]string{"WM_PROTOCOLS", "WM_DELETE_WINDOW", "_NET_WM_NAME", "UTF8_STRING"}, false, atoms[:]); err != nil { | |
| 51 | + | return err | |
| 52 | + | } | |
| 53 | + | protocols, deleteWin, netWMName, utf8 := xproto.Atom(atoms[0]), xproto.Atom(atoms[1]), xproto.Atom(atoms[2]), xproto.Atom(atoms[3]) | |
| 54 | + | ||
| 55 | + | b := c.Buf() | |
| 56 | + | xwin, err := c.NewXID() | |
| 57 | + | if err != nil { | |
| 58 | + | return err | |
| 59 | + | } | |
| 60 | + | xgc, err := c.NewXID() | |
| 61 | + | if err != nil { | |
| 62 | + | return err | |
| 63 | + | } | |
| 64 | + | win, gc := xproto.Window(xwin), xproto.Gcontext(xgc) | |
| 65 | + | ||
| 66 | + | var v xproto.CreateWindowValues | |
| 67 | + | v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify | xproto.EventMaskFocusChange) | |
| 68 | + | xproto.CreateWindow(b, 0, win, xproto.Window(c.Root()), 0, 0, uint16(w), uint16(h), 0, xproto.WindowClassInputOutput, 0, &v) | |
| 69 | + | ||
| 70 | + | wmClass := []byte("x11probe\x00" + *class + "\x00") | |
| 71 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, win, xproto.AtomWmClass, xproto.AtomString, 8, uint32(len(wmClass)), wmClass) | |
| 72 | + | name := []byte(*title) | |
| 73 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, win, netWMName, utf8, 8, uint32(len(name)), name) | |
| 74 | + | var del [4]byte | |
| 75 | + | binary.LittleEndian.PutUint32(del[:], uint32(deleteWin)) | |
| 76 | + | xproto.ChangeProperty(b, xproto.PropModeReplace, win, protocols, xproto.AtomAtom, 32, 1, del[:]) | |
| 77 | + | ||
| 78 | + | var gv xproto.CreateGCValues | |
| 79 | + | gv.SetForeground(green) | |
| 80 | + | xproto.CreateGC(b, gc, xproto.Drawable(win), &gv) | |
| 81 | + | if err := c.Flush(); err != nil { | |
| 82 | + | return err | |
| 83 | + | } | |
| 84 | + | time.Sleep(*sleep) | |
| 85 | + | xproto.MapWindow(b, win) | |
| 86 | + | ||
| 87 | + | mapped, done := false, false | |
| 88 | + | cw, ch := uint16(w), uint16(h) // current size, tracked from ConfigureNotify | |
| 89 | + | hd := &xturbo.Handler{ | |
| 90 | + | OnEvent: func(ev *xturbo.Event) { | |
| 91 | + | switch ev.Kind { | |
| 92 | + | case xproto.KindExpose: | |
| 93 | + | xproto.PolyFillRectangle(b, xproto.Drawable(win), gc, []xproto.Rectangle{{Width: cw, Height: ch}}) | |
| 94 | + | case xproto.KindConfigureNotify: | |
| 95 | + | e := xproto.ConfigureNotifyOf(ev) | |
| 96 | + | if e.Window() != win { | |
| 97 | + | break | |
| 98 | + | } | |
| 99 | + | cw, ch = e.Width(), e.Height() | |
| 100 | + | if !mapped { | |
| 101 | + | mapped = true | |
| 102 | + | fmt.Printf("mapped %dx%d\n", cw, ch) | |
| 103 | + | } | |
| 104 | + | case xproto.KindFocusIn: | |
| 105 | + | fmt.Println("focus") | |
| 106 | + | case xproto.KindClientMessage: | |
| 107 | + | e := xproto.ClientMessageOf(ev) | |
| 108 | + | if e.Type() == protocols && e.Format() == 32 && e.Data().Data32()[0] == uint32(deleteWin) { | |
| 109 | + | done = true | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | }, | |
| 113 | + | OnError: func(e *xturbo.XError) { fmt.Fprintln(os.Stderr, "x11probe:", e) }, | |
| 114 | + | } | |
| 115 | + | for !done { | |
| 116 | + | events := int16(unix.POLLIN) | |
| 117 | + | if err := c.Flush(); err == xturbo.ErrWouldBlock { | |
| 118 | + | events |= unix.POLLOUT | |
| 119 | + | } else if err != nil { | |
| 120 | + | return err | |
| 121 | + | } | |
| 122 | + | fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}} | |
| 123 | + | if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR { | |
| 124 | + | return err | |
| 125 | + | } | |
| 126 | + | if err := c.Dispatch(hd); err != nil { | |
| 127 | + | return err | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | return nil | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | func parseSize(s string) (w, h int, ok bool) { | |
| 134 | + | ws, hs, found := strings.Cut(s, "x") | |
| 135 | + | if !found { | |
| 136 | + | return 0, 0, false | |
| 137 | + | } | |
| 138 | + | w, err1 := strconv.Atoi(ws) | |
| 139 | + | h, err2 := strconv.Atoi(hs) | |
| 140 | + | return w, h, err1 == nil && err2 == nil && w > 0 && h > 0 && w < 0xffff && h < 0xffff | |
| 141 | + | } | |
A internal/app/xwayland_test.go +282 -0
| @@ -0,0 +1,282 @@ | |||
| 1 | + | //go:build xwayland | |
| 2 | + | ||
| 3 | + | // End-to-end test of the built-in XWM: a real Xwayland and a real X11 client | |
| 4 | + | // (testdata/x11probe) run against a headless NeferWL. It needs Xwayland on | |
| 5 | + | // PATH and a GPU, so it is excluded from `go test ./...` and CI. Run it with | |
| 6 | + | // `make e2e-xwayland`. | |
| 7 | + | package app | |
| 8 | + | ||
| 9 | + | import ( | |
| 10 | + | "context" | |
| 11 | + | "image" | |
| 12 | + | "image/png" | |
| 13 | + | "io" | |
| 14 | + | "os" | |
| 15 | + | "os/exec" | |
| 16 | + | "path/filepath" | |
| 17 | + | "regexp" | |
| 18 | + | "strings" | |
| 19 | + | "sync" | |
| 20 | + | "testing" | |
| 21 | + | "time" | |
| 22 | + | ||
| 23 | + | "github.com/bnema/zerowrap" | |
| 24 | + | ||
| 25 | + | "github.com/bnema/neferwl/internal/adapters/config" | |
| 26 | + | "github.com/bnema/neferwl/internal/adapters/statefile" | |
| 27 | + | "github.com/bnema/neferwl/internal/ports" | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | const ( | |
| 31 | + | xwaylandTimeout = 10 * time.Second | |
| 32 | + | xwaylandW, xwaylandH = 640, 360 | |
| 33 | + | ) | |
| 34 | + | ||
| 35 | + | var displayRe = regexp.MustCompile(`"DISPLAY":"(:\d+)"`) | |
| 36 | + | ||
| 37 | + | // probe is a running x11probe. | |
| 38 | + | type probe struct { | |
| 39 | + | cmd *exec.Cmd | |
| 40 | + | out *lockedBuffer | |
| 41 | + | waited chan error | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | // TestXwaylandProbe: an X11 client is managed by the built-in XWM. It shows | |
| 45 | + | // up in the state with its class and pid, is drawn, tiles next to a second | |
| 46 | + | // one, receives focus and is closed by the close-window bind. | |
| 47 | + | func TestXwaylandProbe(t *testing.T) { | |
| 48 | + | if _, err := exec.LookPath("Xwayland"); err != nil { | |
| 49 | + | t.Skip("Xwayland not on PATH") | |
| 50 | + | } | |
| 51 | + | // The compositor must not read or write the live session's state. | |
| 52 | + | for _, k := range []string{"NEFERWL_STATE", "NEFERWL_SLOT"} { | |
| 53 | + | if v, ok := os.LookupEnv(k); ok { | |
| 54 | + | os.Unsetenv(k) | |
| 55 | + | t.Cleanup(func() { os.Setenv(k, v) }) | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | runtime := t.TempDir() | |
| 59 | + | t.Setenv("XDG_RUNTIME_DIR", runtime) | |
| 60 | + | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |
| 61 | + | shots := t.TempDir() | |
| 62 | + | bin := buildProbe(t) | |
| 63 | + | ||
| 64 | + | script, scriptW := io.Pipe() | |
| 65 | + | logs := &lockedBuffer{} | |
| 66 | + | ctx, cancel := context.WithCancel(zerowrap.WithCtx(context.Background(), zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: logs}))) | |
| 67 | + | cfg := config.Defaults() | |
| 68 | + | cfg.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland"} | |
| 69 | + | done := make(chan error, 1) | |
| 70 | + | go func() { | |
| 71 | + | done <- Run(ctx, Options{ | |
| 72 | + | Backend: "headless", NoTerminal: true, Config: cfg, Script: script, | |
| 73 | + | ScreenshotDir: shots, Sizes: [][2]int{{xwaylandW, xwaylandH}}, | |
| 74 | + | }) | |
| 75 | + | }() | |
| 76 | + | t.Cleanup(func() { | |
| 77 | + | if t.Failed() { | |
| 78 | + | t.Logf("compositor log:\n%s", logs.String()) | |
| 79 | + | } | |
| 80 | + | cancel() | |
| 81 | + | _ = scriptW.Close() | |
| 82 | + | select { | |
| 83 | + | case err := <-done: | |
| 84 | + | if err != nil { | |
| 85 | + | t.Errorf("Run: %v", err) | |
| 86 | + | } | |
| 87 | + | case <-time.After(5 * time.Second): | |
| 88 | + | t.Error("Run did not stop") | |
| 89 | + | } | |
| 90 | + | }) | |
| 91 | + | ||
| 92 | + | var display string | |
| 93 | + | until(t, "X11 display in the log", func() bool { | |
| 94 | + | if m := displayRe.FindStringSubmatch(logs.String()); m != nil { | |
| 95 | + | display = m[1] | |
| 96 | + | return true | |
| 97 | + | } | |
| 98 | + | return false | |
| 99 | + | }) | |
| 100 | + | statePath, err := statefile.Path(runtime, "wayland-1") | |
| 101 | + | if err != nil { | |
| 102 | + | t.Fatal(err) | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | first := startProbe(t, bin, display, "-title", "one") | |
| 106 | + | until(t, "probe one mapped", func() bool { return strings.Contains(first.out.String(), "mapped ") }) | |
| 107 | + | ||
| 108 | + | var st statefile.State | |
| 109 | + | until(t, "one visible X11Probe window", func() bool { | |
| 110 | + | st = readState(statePath) | |
| 111 | + | return len(st.Windows) == 1 && st.Windows[0].AppID == "X11Probe" && st.Windows[0].Visible | |
| 112 | + | }) | |
| 113 | + | if w := st.Windows[0]; w.PID != first.cmd.Process.Pid { | |
| 114 | + | t.Errorf("window pid %d, want %d", w.PID, first.cmd.Process.Pid) | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | // The client draws green and the compositor shows it. | |
| 118 | + | id := st.Windows[0].ID | |
| 119 | + | until(t, "green window in latest.png", func() bool { | |
| 120 | + | r, ok := tileRect(readState(statePath), id) | |
| 121 | + | return ok && greenShare(filepath.Join(shots, "latest.png"), r) >= 0.5 | |
| 122 | + | }) | |
| 123 | + | ||
| 124 | + | // Focus round trip: the compositor focused the window, the client saw it. | |
| 125 | + | until(t, "probe one focus", func() bool { return strings.Contains(first.out.String(), "focus") }) | |
| 126 | + | ||
| 127 | + | // A second probe tiles beside the first. | |
| 128 | + | second := startProbe(t, bin, display, "-title", "two") | |
| 129 | + | until(t, "two tiled windows", func() bool { | |
| 130 | + | st = readState(statePath) | |
| 131 | + | if len(st.Windows) != 2 { | |
| 132 | + | return false | |
| 133 | + | } | |
| 134 | + | for _, w := range st.Windows { | |
| 135 | + | if w.AppID != "X11Probe" || !w.Visible || w.Floating { | |
| 136 | + | return false | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | return true | |
| 140 | + | }) | |
| 141 | + | until(t, "probe two focus", func() bool { return strings.Contains(second.out.String(), "focus") }) | |
| 142 | + | ||
| 143 | + | // The close-window bind asks the focused (second) probe to close. | |
| 144 | + | if _, err := io.WriteString(scriptW, "key Super+q\n"); err != nil { | |
| 145 | + | t.Fatal(err) | |
| 146 | + | } | |
| 147 | + | select { | |
| 148 | + | case err := <-second.waited: | |
| 149 | + | if err != nil { | |
| 150 | + | t.Errorf("probe two exited with %v, want 0", err) | |
| 151 | + | } | |
| 152 | + | case <-time.After(2 * time.Second): | |
| 153 | + | t.Fatal("probe two did not exit within 2s of Super+q") | |
| 154 | + | } | |
| 155 | + | until(t, "one window left", func() bool { | |
| 156 | + | st = readState(statePath) | |
| 157 | + | return len(st.Windows) == 1 && st.Windows[0].PID == first.cmd.Process.Pid | |
| 158 | + | }) | |
| 159 | + | select { | |
| 160 | + | case err := <-first.waited: | |
| 161 | + | t.Fatalf("probe one exited (%v) though only the focused probe was closed", err) | |
| 162 | + | default: | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // buildProbe builds testdata/x11probe. | |
| 167 | + | func buildProbe(t *testing.T) string { | |
| 168 | + | t.Helper() | |
| 169 | + | bin := filepath.Join(t.TempDir(), "x11probe") | |
| 170 | + | build := exec.Command("go", "build", "-o", bin, "./testdata/x11probe") | |
| 171 | + | build.Env = append(os.Environ(), "CGO_ENABLED=0") | |
| 172 | + | if out, err := build.CombinedOutput(); err != nil { | |
| 173 | + | t.Fatalf("build x11probe: %v: %s", err, out) | |
| 174 | + | } | |
| 175 | + | return bin | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // startProbe starts x11probe on display, with X11 as its only display | |
| 179 | + | // server, and stops it when the test ends. | |
| 180 | + | func startProbe(t *testing.T, bin, display string, args ...string) *probe { | |
| 181 | + | t.Helper() | |
| 182 | + | p := &probe{out: &lockedBuffer{}, waited: make(chan error, 1)} | |
| 183 | + | p.cmd = exec.Command(bin, args...) | |
| 184 | + | p.cmd.Env = append(envWithout("DISPLAY", "WAYLAND_DISPLAY", "NEFERWL_STATE", "NEFERWL_SLOT"), "DISPLAY="+display) | |
| 185 | + | p.cmd.Stdout = p.out | |
| 186 | + | stderr := &lockedBuffer{} | |
| 187 | + | p.cmd.Stderr = stderr | |
| 188 | + | if err := p.cmd.Start(); err != nil { | |
| 189 | + | t.Fatal(err) | |
| 190 | + | } | |
| 191 | + | var exited sync.Once | |
| 192 | + | go func() { p.waited <- p.cmd.Wait() }() | |
| 193 | + | t.Cleanup(func() { | |
| 194 | + | exited.Do(func() { | |
| 195 | + | _ = p.cmd.Process.Kill() | |
| 196 | + | select { | |
| 197 | + | case <-p.waited: | |
| 198 | + | case <-time.After(2 * time.Second): | |
| 199 | + | } | |
| 200 | + | }) | |
| 201 | + | if s := stderr.String(); s != "" { | |
| 202 | + | t.Logf("x11probe stderr: %s", s) | |
| 203 | + | } | |
| 204 | + | }) | |
| 205 | + | return p | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | func envWithout(keys ...string) []string { | |
| 209 | + | var env []string | |
| 210 | + | next: | |
| 211 | + | for _, kv := range os.Environ() { | |
| 212 | + | for _, k := range keys { | |
| 213 | + | if strings.HasPrefix(kv, k+"=") { | |
| 214 | + | continue next | |
| 215 | + | } | |
| 216 | + | } | |
| 217 | + | env = append(env, kv) | |
| 218 | + | } | |
| 219 | + | return env | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // until polls cond every 20 ms and fails the test after xwaylandTimeout. | |
| 223 | + | func until(t *testing.T, what string, cond func() bool) { | |
| 224 | + | t.Helper() | |
| 225 | + | for deadline := time.Now().Add(xwaylandTimeout); !cond(); time.Sleep(20 * time.Millisecond) { | |
| 226 | + | if time.Now().After(deadline) { | |
| 227 | + | t.Fatalf("timeout waiting for %s", what) | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | // readState returns the current state file, or an empty state while it does | |
| 233 | + | // not exist or is being replaced. | |
| 234 | + | func readState(path string) statefile.State { | |
| 235 | + | st, err := statefile.Read(path) | |
| 236 | + | if err != nil { | |
| 237 | + | return statefile.State{} | |
| 238 | + | } | |
| 239 | + | return st | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | // tileRect finds the rectangle of window id in the tiled columns. | |
| 243 | + | func tileRect(st statefile.State, id uint64) (image.Rectangle, bool) { | |
| 244 | + | for _, ws := range st.Workspaces { | |
| 245 | + | for _, c := range ws.Columns { | |
| 246 | + | for _, w := range c.Windows { | |
| 247 | + | if w.ID == id { | |
| 248 | + | return image.Rect(w.X, w.Y, w.X+w.Width, w.Y+w.Height), true | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | return image.Rectangle{}, false | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | // greenShare is the fraction of pixels of r in the PNG at path that are | |
| 257 | + | // clearly green; 0 when the image cannot be read yet. | |
| 258 | + | func greenShare(path string, r image.Rectangle) float64 { | |
| 259 | + | f, err := os.Open(path) | |
| 260 | + | if err != nil { | |
| 261 | + | return 0 | |
| 262 | + | } | |
| 263 | + | defer f.Close() | |
| 264 | + | img, err := png.Decode(f) | |
| 265 | + | if err != nil { | |
| 266 | + | return 0 | |
| 267 | + | } | |
| 268 | + | r = r.Intersect(img.Bounds()) | |
| 269 | + | if r.Empty() { | |
| 270 | + | return 0 | |
| 271 | + | } | |
| 272 | + | n := 0 | |
| 273 | + | for y := r.Min.Y; y < r.Max.Y; y++ { | |
| 274 | + | for x := r.Min.X; x < r.Max.X; x++ { | |
| 275 | + | cr, cg, cb, _ := img.At(x, y).RGBA() | |
| 276 | + | if cg>>8 > 200 && cr>>8 < 60 && cb>>8 < 60 { | |
| 277 | + | n++ | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | return float64(n) / float64(r.Dx()*r.Dy()) | |
| 282 | + | } | |
M internal/core/core.go +6 -4
| @@ -103,9 +103,11 @@ type Core struct { | |||
| 103 | 103 | binds map[binding]Action | |
| 104 | 104 | pressed map[string]bool | |
| 105 | 105 | configures configures | |
| 106 | - | pointer WindowID | |
| 107 | - | grab WindowID | |
| 108 | - | buttons map[uint32]bool | |
| 106 | + | // placed is the last position (X, Y of the Rect) sent in a PlaceWindow, per window. | |
| 107 | + | placed map[WindowID]Rect | |
| 108 | + | pointer WindowID | |
| 109 | + | grab WindowID | |
| 110 | + | buttons map[uint32]bool | |
| 109 | 111 | // drag is the pointer drag in progress (drag.go); swallow holds the | |
| 110 | 112 | // buttons whose release no client must see. mods are the modifiers | |
| 111 | 113 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | |
| @@ -417,7 +419,7 @@ func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) { | |||
| 417 | 419 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | |
| 418 | 420 | } | |
| 419 | 421 | // A placeholder screen holds windows until the first output arrives. | |
| 420 | - | c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), ping: make(chan chan struct{}), out: newOutbox()} | |
| 422 | + | c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures(), placed: map[WindowID]Rect{}, ping: make(chan chan struct{}), out: newOutbox()} | |
| 421 | 423 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | |
| 422 | 424 | if err := c.apply(cfg); err != nil { | |
| 423 | 425 | return nil, err | |
M internal/core/core_test.go +4 -0
| @@ -38,6 +38,10 @@ func command(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand { | |||
| 38 | 38 | for { | |
| 39 | 39 | switch v := receive(t, ch); v.(type) { | |
| 40 | 40 | case nil, ports.SetOutputs, ports.UserActivity: | |
| 41 | + | case ports.PlaceWindow: | |
| 42 | + | // Only Positioned (X11) windows are placed; every scenario | |
| 43 | + | // using this helper maps xdg windows. | |
| 44 | + | t.Fatalf("unexpected %+v", v) | |
| 41 | 45 | default: | |
| 42 | 46 | return v | |
| 43 | 47 | } | |
A internal/core/place_test.go +80 -0
| @@ -0,0 +1,80 @@ | |||
| 1 | + | package core_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/core" | |
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // places collects the PlaceWindow commands queued so far. | |
| 12 | + | func places(commands chan ports.ClientCommand) (out []ports.PlaceWindow) { | |
| 13 | + | for len(commands) > 0 { | |
| 14 | + | if v, ok := (<-commands).(ports.PlaceWindow); ok { | |
| 15 | + | out = append(out, v) | |
| 16 | + | } | |
| 17 | + | } | |
| 18 | + | return out | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | func TestPlaceWindow(t *testing.T) { | |
| 22 | + | cfg := altCmdDefaults() | |
| 23 | + | cfg.Border.Width = 0 | |
| 24 | + | cfg.Layout.Gaps = 8 | |
| 25 | + | client := make(chan ports.ClientEvent, 16) | |
| 26 | + | input := make(chan ports.InputEvent, 16) | |
| 27 | + | output := make(chan ports.OutputEvent, 8) | |
| 28 | + | commands := make(chan ports.ClientCommand, 256) | |
| 29 | + | scenes := make(chan []ports.Scene, 1) | |
| 30 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 31 | + | if err != nil { | |
| 32 | + | t.Fatal(err) | |
| 33 | + | } | |
| 34 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 35 | + | defer cancel() | |
| 36 | + | go func() { _ = c.Run(ctx) }() | |
| 37 | + | output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}} | |
| 38 | + | scene(t, scenes) | |
| 39 | + | ||
| 40 | + | // An xdg window is never placed. | |
| 41 | + | client <- ports.WindowMapped{ID: 1} | |
| 42 | + | scene(t, scenes) | |
| 43 | + | if got := places(commands); len(got) != 0 { | |
| 44 | + | t.Fatal(got) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // A positioned window is placed once on map. | |
| 48 | + | client <- ports.WindowMapped{ID: 2, Positioned: true} | |
| 49 | + | scene(t, scenes) | |
| 50 | + | first := places(commands) | |
| 51 | + | if len(first) != 1 || first[0].ID != 2 { | |
| 52 | + | t.Fatal(first) | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // The same layout sends nothing more. | |
| 56 | + | client <- ports.WindowMapped{ID: 1} | |
| 57 | + | scene(t, scenes) | |
| 58 | + | if got := places(commands); len(got) != 0 { | |
| 59 | + | t.Fatal(got) | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // A position change sends it again, for the positioned window only. | |
| 63 | + | client <- ports.WindowMapped{ID: 3} | |
| 64 | + | scene(t, scenes) | |
| 65 | + | moved := places(commands) | |
| 66 | + | if len(moved) != 1 || moved[0].ID != 2 || (moved[0].X == first[0].X && moved[0].Y == first[0].Y) { | |
| 67 | + | t.Fatal(first, moved) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // Unmap prunes it: mapping again places from nothing. | |
| 71 | + | client <- ports.WindowUnmapped{ID: 2} | |
| 72 | + | scene(t, scenes) | |
| 73 | + | places(commands) | |
| 74 | + | client <- ports.WindowMapped{ID: 2, Positioned: true} | |
| 75 | + | scene(t, scenes) | |
| 76 | + | again := places(commands) | |
| 77 | + | if len(again) != 1 || again[0].ID != 2 { | |
| 78 | + | t.Fatal(again) | |
| 79 | + | } | |
| 80 | + | } | |
M internal/core/scene.go +10 -0
| @@ -116,6 +116,16 @@ func (c *Core) sceneFor(ctx context.Context, i int, sc *screen, only *screen, ca | |||
| 116 | 116 | } | |
| 117 | 117 | c.configures.mark(v) | |
| 118 | 118 | } | |
| 119 | + | if !p.Hidden && p.Preview == 0 && c.windows.lookup(p.ID).client.Positioned { | |
| 120 | + | cr := c.clientRect(ps) | |
| 121 | + | at := Rect{X: sc.x + cr.X, Y: sc.y + cr.Y} | |
| 122 | + | if old, ok := c.placed[p.ID]; !ok || old != at { | |
| 123 | + | if err := c.command(ctx, ports.PlaceWindow{ID: p.ID, X: at.X, Y: at.Y}); err != nil { | |
| 124 | + | return ports.Scene{}, err | |
| 125 | + | } | |
| 126 | + | c.placed[p.ID] = at | |
| 127 | + | } | |
| 128 | + | } | |
| 119 | 129 | } | |
| 120 | 130 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | |
| 121 | 131 | if icon, ok := c.dataDragIconOn(i, sc); ok { | |
M internal/core/transition.go +1 -0
| @@ -453,6 +453,7 @@ func (c *Core) unmapWindow(v ports.WindowUnmapped) { | |||
| 453 | 453 | // The window is unmapped: a toplevel that maps again keeps its ID and | |
| 454 | 454 | // must be configured from nothing (a leaving entry keeps no configure). | |
| 455 | 455 | c.configures.forget(v.ID) | |
| 456 | + | delete(c.placed, v.ID) | |
| 456 | 457 | sc, w := c.screenOf(v.ID) | |
| 457 | 458 | if sc == nil { | |
| 458 | 459 | return | |
M internal/ports/client_commands.go +10 -0
| @@ -47,6 +47,16 @@ type ConfigureWindow struct { | |||
| 47 | 47 | ||
| 48 | 48 | func (ConfigureWindow) clientCommand() {} | |
| 49 | 49 | ||
| 50 | + | // PlaceWindow carries core → wayland the position of a Positioned window's | |
| 51 | + | // client rect, in layout logical coordinates (output origin included). It | |
| 52 | + | // follows the ConfigureWindow of a frame and is sent once per change. | |
| 53 | + | type PlaceWindow struct { | |
| 54 | + | ID WindowID | |
| 55 | + | X, Y int | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func (PlaceWindow) clientCommand() {} | |
| 59 | + | ||
| 50 | 60 | // OutputPlacement is one output in the global layout. X, Y, Width and | |
| 51 | 61 | // Height are logical (after Transform: they swap for 90/270); | |
| 52 | 62 | // physical = logical × Scale. Info.Width and Info.Height are the physical | |
M internal/ports/client_events.go +3 -0
| @@ -20,6 +20,9 @@ type WindowMapped struct { | |||
| 20 | 20 | // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over | |
| 21 | 21 | // it. | |
| 22 | 22 | Parent WindowID | |
| 23 | + | // Positioned windows are placed by core: it sends PlaceWindow with the | |
| 24 | + | // client rect origin. Set only by the X11 host, false for xdg. | |
| 25 | + | Positioned bool | |
| 23 | 26 | // Remap is set when the toplevel mapped before and was unmapped (a tray | |
| 24 | 27 | // app restored): window rules only place its first map. | |
| 25 | 28 | Remap bool | |
M internal/ports/config.go +17 -3
| @@ -101,9 +101,8 @@ type Config struct { | |||
| 101 | 101 | Command []string | |
| 102 | 102 | AutoOpen string | |
| 103 | 103 | } | |
| 104 | - | // Xwayland is the xwayland-satellite binary serving X11 clients; empty | |
| 105 | - | // disables X11. | |
| 106 | - | Xwayland string | |
| 104 | + | // Xwayland selects the X11 launcher. | |
| 105 | + | Xwayland XwaylandConfig | |
| 107 | 106 | // Startup are commands run once when the session starts, in order. | |
| 108 | 107 | // One startup line may hold several, split by SplitCommands. | |
| 109 | 108 | Startup [][]string | |
| @@ -298,3 +297,18 @@ type WindowRule struct { | |||
| 298 | 297 | // (1 to 16; 0 leaves it automatic). | |
| 299 | 298 | Cells int | |
| 300 | 299 | } | |
| 300 | + | ||
| 301 | + | // XwaylandBackend selects how X11 clients are served. | |
| 302 | + | type XwaylandBackend uint8 | |
| 303 | + | ||
| 304 | + | const ( | |
| 305 | + | XwaylandOff XwaylandBackend = iota | |
| 306 | + | XwaylandSatellite | |
| 307 | + | XwaylandBuiltin | |
| 308 | + | ) | |
| 309 | + | ||
| 310 | + | // XwaylandConfig selects the launcher and its executable. | |
| 311 | + | type XwaylandConfig struct { | |
| 312 | + | Backend XwaylandBackend | |
| 313 | + | Binary string | |
| 314 | + | } | |