Merge 17529ce1e512f84fe75f8eb022e1118eabbe015f into 3c13b3d3cf1deae3f997dd5bb3e9e0a55049380f
74 files · +11381 -144
M .mockery.yml +20 -0
| @@ -182,3 +182,23 @@ packages: | |||||
| 182 | dir: internal/adapters/screencast | 182 | dir: internal/adapters/screencast | ||
| 183 | pkgname: screencast | 183 | pkgname: screencast | ||
| 184 | filename: pipewire_opener_mock_test.go | 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 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | ||
| 4 | TTY_TIMEOUT ?= 0 | 4 | TTY_TIMEOUT ?= 0 | ||
| @@ -26,6 +26,7 @@ test: | |||||
| 26 | CGO_ENABLED=0 go test ./... | 26 | CGO_ENABLED=0 go test ./... | ||
| 27 | vet: | 27 | vet: | ||
| 28 | CGO_ENABLED=0 go vet ./... | 28 | CGO_ENABLED=0 go vet ./... | ||
| 29 | + | CGO_ENABLED=0 go vet -tags xwayland ./internal/app | |||
| 29 | race: | 30 | race: | ||
| 30 | # cgo is enabled only for the race test binary. | 31 | # cgo is enabled only for the race test binary. | ||
| 31 | CGO_ENABLED=1 go test -race ./... | 32 | CGO_ENABLED=1 go test -race ./... | ||
| @@ -69,7 +70,8 @@ adapter-imports: | |||||
| 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | ||
| 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 71 | perf-check: | 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 | check: vet test arch fakes-check log-check perf-check | 75 | check: vet test arch fakes-check log-check perf-check | ||
| 74 | 76 | ||||
| 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 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 | color-check: | 79 | color-check: | ||
| 78 | CGO_ENABLED=0 go test -tags colorcheck ./internal/app -run '^TestColorCheck' -count=1 -v | 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 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | 87 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | ||
| 81 | # come from the module proxy in prepare(); the build itself runs offline. | 88 | # come from the module proxy in prepare(); the build itself runs offline. | ||
| 82 | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | 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 | - Explicit sync carries client fences to the GPU and KMS, enabled by default with no flag. HDR10 on capable displays includes configurable SDR brightness. | 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 | - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects. | 150 | - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects. | ||
| 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. | 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 | ### Windows and workspaces | 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 | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | 27 | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | ||
| 28 | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | 28 | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | ||
| 29 | | `startup` | none | Commands run at session start, separated by `;` | | 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 | | `background` | `#111111` | Background color | | 31 | | `background` | `#111111` | Background color | | ||
| 32 | | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps | | 32 | | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps | | ||
| 33 | | `border.width` | `2` | Border width in pixels; `0` hides borders | | 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 | switches and mouse focus do not trigger a flash. | 103 | switches and mouse focus do not trigger a flash. | ||
| 104 | It runs when `animations = on`. | 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 | ## Automatic terminal | 108 | ## Automatic terminal | ||
| 107 | 109 | ||||
| 108 | The command comes from `terminal`, then `$TERMINAL`, then `foot`. | 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 | ## X11 apps | 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 | ```text | 57 | ```text | ||
| 58 | xwayland = off # no X11 support | 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 | ## Session locking | 63 | ## Session locking | ||
| 62 | 64 | ||||
| 63 | 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. | 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 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | 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 | ## Colour check | 78 | ## Colour check | ||
| 66 | 79 | ||||
| 67 | `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`. | 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 | go 1.27 | 3 | go 1.27 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | + | bnema.dev/xturbo v0.1.0 | |||
| 6 | github.com/bnema/go-wayland-bindings v0.1.0 | 7 | github.com/bnema/go-wayland-bindings v0.1.0 | ||
| 7 | github.com/bnema/kvconf v0.1.2 | 8 | github.com/bnema/kvconf v0.1.2 | ||
| 8 | - | github.com/bnema/purego v0.13.0-bnema.1 | 9 | + | github.com/bnema/purego v0.13.0-bnema.2 |
| 9 | - | github.com/bnema/purego-libwayland v0.11.0 | 10 | + | github.com/bnema/purego-libwayland v0.12.0 |
| 10 | github.com/bnema/purego-pipewire v0.2.0 | 11 | github.com/bnema/purego-pipewire v0.2.0 | ||
| 11 | github.com/bnema/purego-vulkan v0.6.0 | 12 | github.com/bnema/purego-vulkan v0.6.0 | ||
| 12 | github.com/bnema/wlturbo v0.5.0 | 13 | github.com/bnema/wlturbo v0.5.0 | ||
| @@ -16,7 +17,7 @@ require ( | |||||
| 16 | github.com/rs/zerolog v1.35.1 | 17 | github.com/rs/zerolog v1.35.1 | ||
| 17 | github.com/stretchr/testify v1.12.1 | 18 | github.com/stretchr/testify v1.12.1 | ||
| 18 | golang.org/x/image v0.46.0 | 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 | golang.org/x/term v0.46.0 | 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 | // Hint mode is experimental: off unless the config turns it on. | 167 | // Hint mode is experimental: off unless the config turns it on. | ||
| 168 | c.Hint.Enable = false | 168 | c.Hint.Enable = false | ||
| 169 | c.Hint.TextClick = ports.ModCtrl | ports.ModShift | 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 | c.Background.Color = "#111111" | 171 | c.Background.Color = "#111111" | ||
| 172 | c.Floating.Dim = 0.3 | 172 | c.Floating.Dim = 0.3 | ||
| 173 | c.Stash.Width = 80 | 173 | c.Stash.Width = 80 | ||
| @@ -726,11 +726,15 @@ func set(c *ports.Config, key, v string) error { | |||||
| 726 | case "xwayland": | 726 | case "xwayland": | ||
| 727 | switch { | 727 | switch { | ||
| 728 | case v == "off": | 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 | case v == "" || strings.ContainsAny(v, " \t"): | 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 | default: | 736 | default: | ||
| 733 | - | c.Xwayland = v | 737 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: v} |
| 734 | } | 738 | } | ||
| 735 | case "background": | 739 | case "background": | ||
| 736 | if !color.MatchString(v) { | 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 | func TestXwayland(t *testing.T) { | 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 | t.Fatal(Defaults().Xwayland) | 344 | t.Fatal(Defaults().Xwayland) | ||
| 345 | } | 345 | } | ||
| 346 | - | if c, w := parseString(t, "xwayland = off"); c.Xwayland != "" || len(w) != 0 { | 346 | + | for _, tt := range []struct { |
| 347 | - | t.Fatal(c.Xwayland, w) | 347 | + | value string |
| 348 | - | } | 348 | + | want ports.XwaylandConfig |
| 349 | - | if c, w := parseString(t, "xwayland = /opt/xwls"); c.Xwayland != "/opt/xwls" || len(w) != 0 { | 349 | + | }{ |
| 350 | - | t.Fatal(c.Xwayland, w) | 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 | // slot keeps its sync file for every lease. No mock stands in the measured | 358 | // slot keeps its sync file for every lease. No mock stands in the measured | ||
| 359 | // closure. | 359 | // closure. | ||
| 360 | func TestCaptureCycleAllocations(t *testing.T) { | 360 | func TestCaptureCycleAllocations(t *testing.T) { | ||
| 361 | + | if raceEnabled { | |||
| 362 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 363 | + | } | |||
| 361 | for _, tc := range []struct { | 364 | for _, tc := range []struct { | ||
| 362 | name string | 365 | name string | ||
| 363 | new func(*testing.T) *Renderer | 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 | // A new scene Seq with one of two windows moved is a partial redraw seeded by | 16 | // A new scene Seq with one of two windows moved is a partial redraw seeded by | ||
| 17 | // sceneDelta and allocates nothing either. | 17 | // sceneDelta and allocates nothing either. | ||
| 18 | func TestRenderSteadyStateAllocations(t *testing.T) { | 18 | func TestRenderSteadyStateAllocations(t *testing.T) { | ||
| 19 | + | if raceEnabled { | |||
| 20 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 21 | + | } | |||
| 19 | for _, tc := range []struct { | 22 | for _, tc := range []struct { | ||
| 20 | name string | 23 | name string | ||
| 21 | transform ports.BufferTransform | 24 | transform ports.BufferTransform | ||
| @@ -145,6 +148,9 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||||
| 145 | // keeps one alloc of headroom for drivers whose import path differs. | 148 | // keeps one alloc of headroom for drivers whose import path differs. | ||
| 146 | // Skipped without a GPU or /dev/udmabuf. | 149 | // Skipped without a GPU or /dev/udmabuf. | ||
| 147 | func TestRenderSteadyStateDMABufAllocations(t *testing.T) { | 150 | func TestRenderSteadyStateDMABufAllocations(t *testing.T) { | ||
| 151 | + | if raceEnabled { | |||
| 152 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 153 | + | } | |||
| 148 | r, err := New(80, 32) | 154 | r, err := New(80, 32) | ||
| 149 | if err != nil { | 155 | if err != nil { | ||
| 150 | t.Skipf("Vulkan unavailable: %v", err) | 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 | var w *window | 67 | var w *window | ||
| 68 | if reason == "" { | 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 | reason = "not_toplevel" | 70 | reason = "not_toplevel" | ||
| 71 | } | 71 | } | ||
| 72 | } | 72 | } | ||
| @@ -116,8 +116,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) { | |||||
| 116 | 116 | ||||
| 117 | // focusClient is the client with the keyboard focus. | 117 | // focusClient is the client with the keyboard focus. | ||
| 118 | func (s *Server) focusClient() server.Client { | 118 | func (s *Server) focusClient() server.Client { | ||
| 119 | - | if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() { | 119 | + | if w := s.windows[s.seat.focused]; w != nil && w.alive() { |
| 120 | - | return w.xdg.resource.Client() | 120 | + | return w.client() |
| 121 | } | 121 | } | ||
| 122 | if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() { | 122 | if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() { | ||
| 123 | return l.resource.Client() | 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 | return | 61 | return | ||
| 62 | } | 62 | } | ||
| 63 | w := s.windows[s.seat.pointerFocus] | 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 | return | 65 | return | ||
| 66 | } | 66 | } | ||
| 67 | // Surface-local to window-local (core's, from the geometry origin). | 67 | // Surface-local to window-local (core's, from the geometry origin). | ||
| 68 | - | g := w.xdg.geometry | 68 | + | g := w.geometry() |
| 69 | s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)}) | 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 | if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | 80 | if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | ||
| 81 | return false | 81 | return false | ||
| 82 | } | 82 | } | ||
| 83 | - | if !w.mapped || !w.xdg.resource.Resource.Alive() { | 83 | + | if !w.mapped || !w.alive() { |
| 84 | return false | 84 | return false | ||
| 85 | } | 85 | } | ||
| 86 | sent := false | 86 | sent := false | ||
| 87 | usec := uint64(c.Time / time.Microsecond) | 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 | if rel.Resource.Alive() { | 89 | if rel.Resource.Alive() { | ||
| 90 | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) | 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 | sent = true | 91 | sent = true | ||
| @@ -272,7 +272,7 @@ func (s *Server) updateConstraint() { | |||||
| 272 | } | 272 | } | ||
| 273 | var want *constraint | 273 | var want *constraint | ||
| 274 | if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused { | 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 | want = c | 276 | want = c | ||
| 277 | } | 277 | } | ||
| 278 | } | 278 | } | ||
| @@ -298,8 +298,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 298 | if s.protected() { | 298 | if s.protected() { | ||
| 299 | return | 299 | return | ||
| 300 | } | 300 | } | ||
| 301 | - | x := c.surface.xdg | 301 | + | w := c.surface.toplevelWindow() |
| 302 | - | if x == nil || x.window == nil { | 302 | + | if w == nil { |
| 303 | return | 303 | return | ||
| 304 | } | 304 | } | ||
| 305 | mode := ports.ConstraintConfine | 305 | mode := ports.ConstraintConfine | ||
| @@ -308,8 +308,9 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 308 | } | 308 | } | ||
| 309 | r := c.region | 309 | r := c.region | ||
| 310 | if r.W > 0 && r.H > 0 { | 310 | if r.W > 0 && r.H > 0 { | ||
| 311 | - | r.X -= x.geometry.X | 311 | + | g := w.geometry() |
| 312 | - | r.Y -= x.geometry.Y | 312 | + | r.X -= g.X |
| 313 | + | r.Y -= g.Y | |||
| 313 | } | 314 | } | ||
| 314 | // Core supports one confinement rect, so this bounds the intersection; | 315 | // Core supports one confinement rect, so this bounds the intersection; | ||
| 315 | // contains uses the exact input region to decide activation. | 316 | // contains uses the exact input region to decide activation. | ||
| @@ -333,17 +334,17 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 333 | r = box | 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 | // contains reports whether the pointer is in the region: a constraint | 340 | // contains reports whether the pointer is in the region: a constraint | ||
| 340 | // activates only there, so it never warps the pointer. | 341 | // activates only there, so it never warps the pointer. | ||
| 341 | func (c *constraint) contains(w *window) bool { | 342 | func (c *constraint) contains(w *window) bool { | ||
| 342 | r := c.region | 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 | all, rects := c.surface.effectiveInput() | 345 | all, rects := c.surface.effectiveInput() | ||
| 345 | if !all { | 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 | inside := false | 348 | inside := false | ||
| 348 | for _, input := range rects { | 349 | for _, input := range rects { | ||
| 349 | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | 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 | // locked reports whether the window's pointer is locked. | 365 | // locked reports whether the window's pointer is locked. | ||
| 365 | func (s *Server) locked(w *window) bool { | 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 | func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) { | 80 | func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) { | ||
| 81 | r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | 81 | r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | ||
| 82 | for _, w := range d.s.windows { | 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 | w.sendConfigure() | 84 | w.sendConfigure() | ||
| 85 | return | 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 | w.modal = modal | 78 | w.modal = modal | ||
| 79 | if w.mapped { | 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 | return "", false | 223 | return "", false | ||
| 224 | } | 224 | } | ||
| 225 | root := toplevelRoot(surf) | 225 | root := toplevelRoot(surf) | ||
| 226 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen || g.server.invisible(surf) { | 226 | + | w := root.toplevelWindow() |
| 227 | + | if w == nil || !w.hasLast || !w.last.Fullscreen || g.server.invisible(surf) { | |||
| 227 | return "", false | 228 | return "", false | ||
| 228 | } | 229 | } | ||
| 229 | - | return root.xdg.window.last.Output, true | 230 | + | return w.last.Output, true |
| 230 | } | 231 | } | ||
| 231 | 232 | ||||
| 232 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | 233 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | ||
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
| @@ -72,7 +72,7 @@ func (*extToplevelList) Destroy(*ext.ExtForeignToplevelListV1) {} | |||||
| 72 | func (s *Server) mappedToplevels() []ports.WindowID { | 72 | func (s *Server) mappedToplevels() []ports.WindowID { | ||
| 73 | ids := make([]ports.WindowID, 0, len(s.windows)) | 73 | ids := make([]ports.WindowID, 0, len(s.windows)) | ||
| 74 | for id, w := range s.windows { | 74 | for id, w := range s.windows { | ||
| 75 | - | if w.toplevel != nil && w.mapped { | 75 | + | if w.isToplevel() && w.mapped { |
| 76 | ids = append(ids, id) | 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 | r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) } | 44 | r.OnDestroy = func() { s.toplevelManagers = removeItem(s.toplevelManagers, m) } | ||
| 45 | ids := make([]ports.WindowID, 0, len(s.windows)) | 45 | ids := make([]ports.WindowID, 0, len(s.windows)) | ||
| 46 | for id, w := range s.windows { | 46 | for id, w := range s.windows { | ||
| 47 | - | if w.toplevel != nil && w.mapped { | 47 | + | if w.isToplevel() && w.mapped { |
| 48 | ids = append(ids, id) | 48 | ids = append(ids, id) | ||
| 49 | } | 49 | } | ||
| 50 | } | 50 | } | ||
| @@ -186,7 +186,7 @@ func toplevelState(w *window, version int32) []byte { | |||||
| 186 | 186 | ||||
| 187 | // toplevelChanged updates every manager after w mapped or changed. | 187 | // toplevelChanged updates every manager after w mapped or changed. | ||
| 188 | func (s *Server) toplevelChanged(w *window) { | 188 | func (s *Server) toplevelChanged(w *window) { | ||
| 189 | - | if w.toplevel == nil || !w.mapped { | 189 | + | if !w.isToplevel() || !w.mapped { |
| 190 | return | 190 | return | ||
| 191 | } | 191 | } | ||
| 192 | for _, m := range s.toplevelManagers { | 192 | for _, m := range s.toplevelManagers { | ||
| @@ -220,7 +220,7 @@ func (s *Server) refreshToplevels() { | |||||
| 220 | for _, l := range s.extToplevelLists { | 220 | for _, l := range s.extToplevelLists { | ||
| 221 | for id, h := range l.handles { | 221 | for id, h := range l.handles { | ||
| 222 | w := s.windows[id] | 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 | l.closed(id) | 224 | l.closed(id) | ||
| 225 | } | 225 | } | ||
| 226 | } | 226 | } | ||
| @@ -231,7 +231,7 @@ func (s *Server) refreshToplevels() { | |||||
| 231 | for _, m := range s.toplevelManagers { | 231 | for _, m := range s.toplevelManagers { | ||
| 232 | for id, h := range m.handles { | 232 | for id, h := range m.handles { | ||
| 233 | w := s.windows[id] | 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 | m.closed(id) | 235 | m.closed(id) | ||
| 236 | } | 236 | } | ||
| 237 | } | 237 | } | ||
| @@ -253,7 +253,7 @@ func (t *toplevelRequests) window() *window { | |||||
| 253 | if t.s.protected() || t.closed { | 253 | if t.s.protected() || t.closed { | ||
| 254 | return nil | 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 | return w | 257 | return w | ||
| 258 | } | 258 | } | ||
| 259 | return nil | 259 | return nil | ||
| @@ -274,7 +274,7 @@ func (t *toplevelRequests) Activate(*wlr.ZwlrForeignToplevelHandleV1, *wayland.S | |||||
| 274 | } | 274 | } | ||
| 275 | func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) { | 275 | func (t *toplevelRequests) Close(*wlr.ZwlrForeignToplevelHandleV1) { | ||
| 276 | if w := t.window(); w != nil { | 276 | if w := t.window(); w != nil { | ||
| 277 | - | w.toplevel.SendClose() | 277 | + | w.sendClose() |
| 278 | } | 278 | } | ||
| 279 | } | 279 | } | ||
| 280 | func (t *toplevelRequests) SetRectangle(r *wlr.ZwlrForeignToplevelHandleV1, _ *wayland.Surface, _, _, w, h int32) { | 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 | func toplevelRoot(surf *surface) *surface { | 183 | func toplevelRoot(surf *surface) *surface { | ||
| 184 | surf = surf.root() | 184 | surf = surf.root() | ||
| 185 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | 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 | return surf | 188 | return surf | ||
| 189 | } | 189 | } | ||
| @@ -193,7 +193,8 @@ func toplevelRoot(surf *surface) *surface { | |||||
| 193 | // for a capture session is invisible. | 193 | // for a capture session is invisible. | ||
| 194 | func (s *Server) invisible(surf *surface) bool { | 194 | func (s *Server) invisible(surf *surface) bool { | ||
| 195 | root := toplevelRoot(surf) | 195 | root := toplevelRoot(surf) | ||
| 196 | - | return root.xdg != nil && root.xdg.window != nil && root.xdg.window.hasLast && !root.xdg.window.last.Visible | 196 | + | w := root.toplevelWindow() |
| 197 | + | return w != nil && w.hasLast && !w.last.Visible | |||
| 197 | } | 198 | } | ||
| 198 | 199 | ||||
| 199 | // suspended reports that nothing draws the toplevel tree, not even a capture | 200 | // suspended reports that nothing draws the toplevel tree, not even a capture | ||
| @@ -201,10 +202,11 @@ func (s *Server) invisible(surf *surface) bool { | |||||
| 201 | // frame callbacks of its output so the capture keeps moving. | 202 | // frame callbacks of its output so the capture keeps moving. | ||
| 202 | func (s *Server) suspended(surf *surface) bool { | 203 | func (s *Server) suspended(surf *surface) bool { | ||
| 203 | root := toplevelRoot(surf) | 204 | root := toplevelRoot(surf) | ||
| 204 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast { | 205 | + | w := root.toplevelWindow() |
| 206 | + | if w == nil || !w.hasLast { | |||
| 205 | return false | 207 | return false | ||
| 206 | } | 208 | } | ||
| 207 | - | return !root.xdg.window.last.Visible && !root.xdg.window.last.Captured | 209 | + | return !w.last.Visible && !w.last.Captured |
| 208 | } | 210 | } | ||
| 209 | 211 | ||||
| 210 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | 212 | // frameOutput is the pacing class for a surface; "" is the 60 Hz | ||
| @@ -212,11 +214,11 @@ func (s *Server) suspended(surf *surface) bool { | |||||
| 212 | func (s *Server) frameOutput(surf *surface) string { | 214 | func (s *Server) frameOutput(surf *surface) string { | ||
| 213 | surf = toplevelRoot(surf) | 215 | surf = toplevelRoot(surf) | ||
| 214 | switch { | 216 | switch { | ||
| 215 | - | case surf.xdg != nil && surf.xdg.window != nil: | 217 | + | case surf.toplevelWindow() != nil: |
| 216 | if s.suspended(surf) { | 218 | if s.suspended(surf) { | ||
| 217 | return suspendedFrameQueue | 219 | return suspendedFrameQueue | ||
| 218 | } | 220 | } | ||
| 219 | - | return surf.xdg.window.last.Output | 221 | + | return surf.toplevelWindow().last.Output |
| 220 | case surf.layer != nil && surf.layer.output != nil: | 222 | case surf.layer != nil && surf.layer.output != nil: | ||
| 221 | return surf.layer.output.name() | 223 | return surf.layer.output.name() | ||
| 222 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | 224 | case surf.lock != nil && surf.lock.output != nil && !surf.lock.closed: | ||
M internal/adapters/wayland/globals.go +10 -1
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | func registerGlobals(d *server.Display, o Options, s *Server) error { | 13 | func registerGlobals(d *server.Display, o Options, s *Server) error { | ||
| 14 | for _, register := range []func() error{ | 14 | for _, register := range []func() error{ | ||
| 15 | func() error { return registerXDG(d, s) }, | 15 | func() error { return registerXDG(d, s) }, | ||
| 16 | + | func() error { return registerXwaylandShell(d, s) }, | |||
| 16 | func() error { return registerLayer(d, s) }, | 17 | func() error { return registerLayer(d, s) }, | ||
| 17 | func() error { | 18 | func() error { | ||
| 18 | // Advertise only with the production owner gate and authoritative | 19 | // Advertise only with the production owner gate and authoritative | ||
| @@ -131,8 +132,16 @@ func (c compositor) CreateSurface(r *wayland.Compositor, id uint32) { | |||||
| 131 | state.identity = c.server.nextSurface | 132 | state.identity = c.server.nextSurface | ||
| 132 | state.wl = w | 133 | state.wl = w | ||
| 133 | c.server.surfaces[w.Resource] = state | 134 | c.server.surfaces[w.Resource] = state | ||
| 135 | + | c.server.surfaceByIdentity[state.identity] = state | |||
| 134 | state.sendScale() | 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 | func (c compositor) CreateRegion(r *wayland.Compositor, id uint32) { | 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 | case srcWindow: | 351 | case srcWindow: | ||
| 352 | // Core's state gives the size; before it, the last configure. | 352 | // Core's state gives the size; before it, the last configure. | ||
| 353 | w := c.s.windows[src.window] | 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 | return nil, image.Rectangle{}, false, false | 355 | return nil, image.Rectangle{}, false, false | ||
| 356 | } | 356 | } | ||
| 357 | name, size := w.last.Output, ports.Rect{W: w.last.Width, H: w.last.Height} | 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 | var parent *window | 31 | var parent *window | ||
| 32 | if parentXDG != nil { | 32 | if parentXDG != nil { | ||
| 33 | for _, w := range s.windows { | 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 | parent = w | 35 | parent = w | ||
| 36 | } | 36 | } | ||
| 37 | } | 37 | } | ||
| @@ -166,7 +166,7 @@ func (p *popup) configure(c ports.ConfigurePopup) { | |||||
| 166 | func (s *Server) topGrab(client server.Client, parent *window) bool { | 166 | func (s *Server) topGrab(client server.Client, parent *window) bool { | ||
| 167 | var top *window | 167 | var top *window | ||
| 168 | for _, w := range s.windows { | 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 | top = w | 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 | surf = surf.root() | 21 | surf = surf.root() | ||
| 22 | // Popups are on their parent's output. | 22 | // Popups are on their parent's output. | ||
| 23 | for surf.xdg != nil && surf.xdg.window != nil && surf.xdg.window.popup != nil && surf.xdg.window.popup.parent != nil { | 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 | name := "" | 26 | name := "" | ||
| 27 | switch { | 27 | switch { | ||
| 28 | - | case surf.xdg != nil && surf.xdg.window != nil: | 28 | + | case surf.toplevelWindow() != nil: |
| 29 | - | name = surf.xdg.window.last.Output | 29 | + | name = surf.toplevelWindow().last.Output |
| 30 | case surf.layer != nil && surf.layer.output != nil: | 30 | case surf.layer != nil && surf.layer.output != nil: | ||
| 31 | return surf.layer.output | 31 | return surf.layer.output | ||
| 32 | case surf.lock != nil: | 32 | case surf.lock != nil: | ||
M internal/adapters/wayland/screencast.go +1 -1
| @@ -146,7 +146,7 @@ func (s *Server) openCast(cmd ports.CastCommand) { | |||||
| 146 | gone := ports.CaptureReasonOutputGone | 146 | gone := ports.CaptureReasonOutputGone | ||
| 147 | if cmd.Window != 0 { | 147 | if cmd.Window != 0 { | ||
| 148 | gone = ports.CaptureReasonWindowGone | 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 | src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen} | 150 | src = &captureSource{kind: srcWindow, window: cmd.Window, gen: w.gen} | ||
| 151 | open.Window = cmd.Window | 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 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | 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 | return nil | 280 | return nil | ||
| 281 | } | 281 | } | ||
| 282 | - | return s.clientKeyboards(w.xdg.resource.Client()) | 282 | + | return s.clientKeyboards(w.client()) |
| 283 | } | 283 | } | ||
| 284 | 284 | ||||
| 285 | // clientKeyboards returns c's wl_keyboards without allocating. The list holds | 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 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | 301 | return l.surface.wl, s.clientKeyboards(l.surface.wl.Client()) | ||
| 302 | } | 302 | } | ||
| 303 | if w := s.windows[id]; w != nil && w.mapped { | 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 | return surf, s.windowKeyboards(w) | 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 | if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) { | 319 | if l := s.lockSurfaces[id]; l != nil && s.lockInputTarget(id) { | ||
| 320 | return l.resource.Client(), s.clientPointers(l.resource.Client()), true | 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() { | 322 | + | if w := s.windows[id]; w != nil && w.mapped && w.alive() { |
| 323 | - | return w.xdg.resource.Client(), s.windowPointers(w), true | 323 | + | return w.client(), s.windowPointers(w), true |
| 324 | } | 324 | } | ||
| 325 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | 325 | if l := s.layers[id]; l != nil && l.mapped && l.resource.Resource.Alive() { | ||
| 326 | return l.resource.Client(), s.layerPointers(l), true | 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 | return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y | 335 | return l.surface.wl, s.clientPointers(l.surface.wl.Client()), x, y | ||
| 336 | } | 336 | } | ||
| 337 | if w := s.windows[id]; w != nil { | 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 | x, y := w.surfacePoint(x, y) | 339 | x, y := w.surfacePoint(x, y) | ||
| 340 | return surf, s.windowPointers(w), x, y | 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 | s.textInputFocus() | 457 | s.textInputFocus() | ||
| 458 | + | s.x11Focus() | |||
| 458 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | 459 | s.log.Debug().Uint64("id", uint64(s.seat.focused)).Msg("keyboard focus") | ||
| 459 | s.updateConstraint() | 460 | s.updateConstraint() | ||
| 460 | } | 461 | } | ||
| @@ -543,10 +544,10 @@ func pointerFrame(p *wayland.Pointer) { | |||||
| 543 | } | 544 | } | ||
| 544 | } | 545 | } | ||
| 545 | func (s *Server) windowPointers(w *window) []*wayland.Pointer { | 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 | return nil | 548 | return nil | ||
| 548 | } | 549 | } | ||
| 549 | - | return s.clientPointers(w.xdg.resource.Client()) | 550 | + | return s.clientPointers(w.client()) |
| 550 | } | 551 | } | ||
| 551 | 552 | ||||
| 552 | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | 553 | func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer { | ||
M internal/adapters/wayland/server.go +36 -16
| @@ -16,6 +16,7 @@ import ( | |||||
| 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | 16 | "github.com/bnema/neferwl/internal/adapters/captureallow" | ||
| 17 | "github.com/bnema/neferwl/internal/adapters/clock" | 17 | "github.com/bnema/neferwl/internal/adapters/clock" | ||
| 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | 18 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | ||
| 19 | + | "github.com/bnema/neferwl/internal/adapters/wayland/xwm" | |||
| 19 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | 20 | "github.com/bnema/neferwl/internal/adapters/workspaceid" | ||
| 20 | "github.com/bnema/neferwl/internal/ports" | 21 | "github.com/bnema/neferwl/internal/ports" | ||
| 21 | "github.com/bnema/purego-libwayland/server" | 22 | "github.com/bnema/purego-libwayland/server" | ||
| @@ -160,13 +161,18 @@ type Server struct { | |||||
| 160 | securityStop int | 161 | securityStop int | ||
| 161 | securityWG sync.WaitGroup | 162 | securityWG sync.WaitGroup | ||
| 162 | securityLive atomic.Int32 | 163 | securityLive atomic.Int32 | ||
| 163 | - | display *server.Display | 164 | + | xwayland xwaylandClient // display owner |
| 164 | - | env procEnv | 165 | + | // xwm manages the X11 windows of the Xwayland client; x11Slots maps its |
| 165 | - | name string | 166 | + | // slots to their windows. Display goroutine only. |
| 166 | - | cleanup func() | 167 | + | xwm *xwm.XWM |
| 167 | - | log zerowrap.Logger | 168 | + | x11Slots map[xwm.Slot]*window |
| 168 | - | logDamage bool | 169 | + | display *server.Display |
| 169 | - | channels Channels | 170 | + | env procEnv |
| 171 | + | name string | |||
| 172 | + | cleanup func() | |||
| 173 | + | log zerowrap.Logger | |||
| 174 | + | logDamage bool | |||
| 175 | + | channels Channels | |||
| 170 | // awaiting holds frame callbacks by output name, due at its next | 176 | // awaiting holds frame callbacks by output name, due at its next | ||
| 171 | // frame (frameDue, or its page flip). | 177 | // frame (frameDue, or its page flip). | ||
| 172 | awaiting map[string][]*wayland.Callback | 178 | awaiting map[string][]*wayland.Callback | ||
| @@ -214,8 +220,11 @@ type Server struct { | |||||
| 214 | // foreignExports are the live xdg-foreign handles (foreign.go). | 220 | // foreignExports are the live xdg-foreign handles (foreign.go). | ||
| 215 | foreignExports map[string]*foreignExport | 221 | foreignExports map[string]*foreignExport | ||
| 216 | surfaces map[*server.Resource]*surface | 222 | surfaces map[*server.Resource]*surface | ||
| 217 | - | buffers map[*server.Resource]clientBuffer | 223 | + | // surfaceByIdentity finds a surface by its identity (xwayland_shell_v1 |
| 218 | - | dmabuf *dmabufGlobal | 224 | + | // pairing); kept with surfaces. |
| 225 | + | surfaceByIdentity map[uint64]*surface | |||
| 226 | + | buffers map[*server.Resource]clientBuffer | |||
| 227 | + | dmabuf *dmabufGlobal | |||
| 219 | // hdrOutputs records confirmed DRM modesets; absent outputs are SDR. | 228 | // hdrOutputs records confirmed DRM modesets; absent outputs are SDR. | ||
| 220 | hdrOutputs map[string]ports.OutputHDR | 229 | hdrOutputs map[string]ports.OutputHDR | ||
| 221 | colorID uint64 | 230 | colorID uint64 | ||
| @@ -366,8 +375,10 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 366 | logDamage: opts.LogDamage, | 375 | logDamage: opts.LogDamage, | ||
| 367 | channels: ch, | 376 | channels: ch, | ||
| 368 | surfaces: make(map[*server.Resource]*surface), | 377 | surfaces: make(map[*server.Resource]*surface), | ||
| 378 | + | surfaceByIdentity: make(map[uint64]*surface), | |||
| 369 | buffers: make(map[*server.Resource]clientBuffer), | 379 | buffers: make(map[*server.Resource]clientBuffer), | ||
| 370 | windows: make(map[ports.WindowID]*window), | 380 | windows: make(map[ports.WindowID]*window), | ||
| 381 | + | x11Slots: make(map[xwm.Slot]*window), | |||
| 371 | layers: make(map[ports.WindowID]*layerSurface), | 382 | layers: make(map[ports.WindowID]*layerSurface), | ||
| 372 | nextWindow: 1, | 383 | nextWindow: 1, | ||
| 373 | eventReady: make(chan struct{}, 1), | 384 | eventReady: make(chan struct{}, 1), | ||
| @@ -498,6 +509,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||||
| 498 | if err = s.openPaceTimer(); err != nil { | 509 | if err = s.openPaceTimer(); err != nil { | ||
| 499 | return abort(err) | 510 | return abort(err) | ||
| 500 | } | 511 | } | ||
| 512 | + | d.SetGlobalFilter(s.globalVisible) | |||
| 501 | if err = registerGlobals(d, opts, s); err != nil { | 513 | if err = registerGlobals(d, opts, s); err != nil { | ||
| 502 | return abort(err) | 514 | return abort(err) | ||
| 503 | } | 515 | } | ||
| @@ -585,6 +597,12 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 585 | } | 597 | } | ||
| 586 | s.castInventoryChanged() | 598 | s.castInventoryChanged() | ||
| 587 | err := s.display.Run(ctx) | 599 | err := s.display.Run(ctx) | ||
| 600 | + | // The display is gone: only the X connection is left to close. | |||
| 601 | + | if s.xwm != nil { | |||
| 602 | + | s.xwm.CloseConn() | |||
| 603 | + | s.xwm = nil | |||
| 604 | + | } | |||
| 605 | + | s.forgetXwayland() | |||
| 588 | s.stopSecurityContexts() | 606 | s.stopSecurityContexts() | ||
| 589 | wg.Wait() | 607 | wg.Wait() | ||
| 590 | return err | 608 | return err | ||
| @@ -774,9 +792,11 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 774 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | 792 | if w := s.windows[c.ID]; w != nil && w.popup != nil { | ||
| 775 | w.popup.dismiss() | 793 | w.popup.dismiss() | ||
| 776 | } | 794 | } | ||
| 795 | + | case ports.PlaceWindow: | |||
| 796 | + | s.x11Place(c) | |||
| 777 | case ports.ConfigureWindow: | 797 | case ports.ConfigureWindow: | ||
| 778 | w := s.windows[c.ID] | 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 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | 800 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window") | ||
| 781 | return | 801 | return | ||
| 782 | } | 802 | } | ||
| @@ -787,16 +807,16 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 787 | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible | 807 | scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible | ||
| 788 | w.last, w.hasLast = c, true | 808 | w.last, w.hasLast = c, true | ||
| 789 | if visibilityChanged { | 809 | if visibilityChanged { | ||
| 790 | - | s.relocateCallbacks(w.xdg.surface) | 810 | + | s.relocateCallbacks(w.surf()) |
| 791 | s.dropFeedbacks(time.Now()) | 811 | s.dropFeedbacks(time.Now()) | ||
| 792 | s.wakePacer() // reconsider FIFO barriers at the new period | 812 | s.wakePacer() // reconsider FIFO barriers at the new period | ||
| 793 | } | 813 | } | ||
| 794 | w.sendConfigure() | 814 | w.sendConfigure() | ||
| 795 | s.toplevelChanged(w) | 815 | s.toplevelChanged(w) | ||
| 796 | - | if scanoutChanged && w.xdg.surface != nil { | 816 | + | if scanoutChanged && w.surf() != nil { |
| 797 | - | s.dmabuf.resendSurface(w.xdg.surface) | 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 | surf.sendTreeScale() | 820 | surf.sendTreeScale() | ||
| 801 | } | 821 | } | ||
| 802 | if scanoutChanged { | 822 | if scanoutChanged { | ||
| @@ -817,10 +837,10 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 817 | s.setShortcutsInhibit(c) | 837 | s.setShortcutsInhibit(c) | ||
| 818 | case ports.CloseWindow: | 838 | case ports.CloseWindow: | ||
| 819 | w := s.windows[c.ID] | 839 | w := s.windows[c.ID] | ||
| 820 | - | if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() { | 840 | + | if w == nil || !w.toplevelAlive() { |
| 821 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | 841 | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window") | ||
| 822 | return | 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 | roleSessionLock | 28 | roleSessionLock | ||
| 29 | // roleDragIcon is the icon of a data drag (dnd.go). | 29 | // roleDragIcon is the icon of a data drag (dnd.go). | ||
| 30 | roleDragIcon | 30 | roleDragIcon | ||
| 31 | + | // roleX11 is the surface of an X11 window paired through | |||
| 32 | + | // xwayland_shell_v1. | |||
| 33 | + | roleX11 | |||
| 31 | ) | 34 | ) | ||
| 32 | 35 | ||||
| 33 | type surface struct { | 36 | type surface struct { | ||
| @@ -47,8 +50,12 @@ type surface struct { | |||||
| 47 | formatOpaque bool | 50 | formatOpaque bool | ||
| 48 | kind roleKind | 51 | kind roleKind | ||
| 49 | xdg *xdgSurface | 52 | xdg *xdgSurface | ||
| 50 | - | layer *layerSurface | 53 | + | // x11win is the X11 window the surface is associated with; x11Serial is |
| 51 | - | lock *lockSurface | 54 | + | // the serial of xwayland_surface_v1.set_serial, 0 until set. |
| 55 | + | x11win *window | |||
| 56 | + | x11Serial uint64 | |||
| 57 | + | layer *layerSurface | |||
| 58 | + | lock *lockSurface | |||
| 52 | // bufferCommitted is permanent, including commits later discarded by FIFO. | 59 | // bufferCommitted is permanent, including commits later discarded by FIFO. | ||
| 53 | bufferCommitted bool | 60 | bufferCommitted bool | ||
| 54 | server *Server | 61 | server *Server | ||
| @@ -192,6 +199,9 @@ func (s *surface) windowID() ports.WindowID { | |||||
| 192 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | 199 | if s.xdg != nil && s.xdg.window != nil && s.xdg.window.mapped { | ||
| 193 | return s.xdg.window.id | 200 | return s.xdg.window.id | ||
| 194 | } | 201 | } | ||
| 202 | + | if s.x11win != nil && s.x11win.mapped { | |||
| 203 | + | return s.x11win.id | |||
| 204 | + | } | |||
| 195 | if s.layer != nil && s.layer.mapped { | 205 | if s.layer != nil && s.layer.mapped { | ||
| 196 | return s.layer.id | 206 | return s.layer.id | ||
| 197 | } | 207 | } | ||
| @@ -566,6 +576,9 @@ func (s *surface) applyCommit(u *update) { | |||||
| 566 | if s.xdg != nil && s.xdg.window != nil { | 576 | if s.xdg != nil && s.xdg.window != nil { | ||
| 567 | s.xdg.window.afterCommit() | 577 | s.xdg.window.afterCommit() | ||
| 568 | } | 578 | } | ||
| 579 | + | if s.x11win != nil { | |||
| 580 | + | s.x11win.afterCommit() | |||
| 581 | + | } | |||
| 569 | if s.lock != nil { | 582 | if s.lock != nil { | ||
| 570 | // Role callbacks precede content construction; publish lock placement | 583 | // Role callbacks precede content construction; publish lock placement | ||
| 571 | // only once validated current dimensions are available. | 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 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | 55 | wire_xdgforeign "github.com/bnema/go-wayland-bindings/server/xdgforeign" | ||
| 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | 56 | wire_xdgoutput "github.com/bnema/go-wayland-bindings/server/xdgoutput" | ||
| 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | 57 | wire_xdgshell "github.com/bnema/go-wayland-bindings/server/xdgshell" | ||
| 58 | + | wire_xwaylandshell "github.com/bnema/go-wayland-bindings/server/xwaylandshell" | |||
| 58 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | 59 | wire_imagecapture "github.com/bnema/neferwl/internal/adapters/wayland/imagecapture" | ||
| 59 | "github.com/bnema/purego-libwayland/server" | 60 | "github.com/bnema/purego-libwayland/server" | ||
| 60 | "github.com/bnema/wlturbo" | 61 | "github.com/bnema/wlturbo" | ||
| @@ -253,6 +254,8 @@ var wireSchemas = map[string]wireSchema{ | |||||
| 253 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | 254 | "zwp_text_input_v3": {wire_textinput.ZwpTextInputV3Interface, &client_textinput.TextInputV3{}, map[uint16]bool{0: true}, nil}, | ||
| 254 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | 255 | "zwp_virtual_keyboard_manager_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardManagerV1Interface, &client_virtualkeyboard.VirtualKeyboardManager{}, nil, nil}, | ||
| 255 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | 256 | "zwp_virtual_keyboard_v1": {wire_virtualkeyboard.ZwpVirtualKeyboardV1Interface, &client_virtualkeyboard.VirtualKeyboard{}, map[uint16]bool{3: true}, nil}, | ||
| 257 | + | "xwayland_shell_v1": {wire_xwaylandshell.XwaylandShellV1Interface, nil, map[uint16]bool{0: true}, nil}, | |||
| 258 | + | "xwayland_surface_v1": {wire_xwaylandshell.XwaylandSurfaceV1Interface, nil, map[uint16]bool{1: true}, nil}, | |||
| 256 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | 259 | "zxdg_output_manager_v1": {wire_xdgoutput.ZxdgOutputManagerV1Interface, &client_xdgoutput.ZxdgOutputManager{}, map[uint16]bool{0: true}, nil}, | ||
| 257 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | 260 | "zxdg_output_v1": {wire_xdgoutput.ZxdgOutputV1Interface, &client_xdgoutput.ZxdgOutput{}, map[uint16]bool{0: true}, nil}, | ||
| 258 | } | 261 | } | ||
A internal/adapters/wayland/x11peer_test.go +298 -0
| @@ -0,0 +1,298 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "io" | |||
| 6 | + | "os" | |||
| 7 | + | "sync" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "golang.org/x/sys/unix" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // x11Peer is a scripted X server speaking the real wire protocol over a | |||
| 15 | + | // socketpair: setup, extensions, atoms, properties, geometry. It records every | |||
| 16 | + | // request. It is a protocol peer, not a double of a project interface. | |||
| 17 | + | type x11Peer struct { | |||
| 18 | + | t *testing.T | |||
| 19 | + | f *os.File | |||
| 20 | + | mu sync.Mutex | |||
| 21 | + | ||||
| 22 | + | seq uint16 | |||
| 23 | + | reqs []x11Req | |||
| 24 | + | atoms map[string]uint32 | |||
| 25 | + | nextAtom uint32 | |||
| 26 | + | props map[uint32]map[uint32][]byte // window -> atom -> typed data (type, format, data) | |||
| 27 | + | propMeta map[uint32]map[uint32][2]uint32 | |||
| 28 | + | done chan struct{} | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | type x11Req struct { | |||
| 32 | + | major, minor uint8 | |||
| 33 | + | raw []byte | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func (r x11Req) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) } | |||
| 37 | + | ||||
| 38 | + | const ( | |||
| 39 | + | x11PeerRoot = 0x100 | |||
| 40 | + | x11Win = 0x500001 | |||
| 41 | + | // Opcodes the tests look for. | |||
| 42 | + | x11OpConfigureWindow = 12 | |||
| 43 | + | x11OpSetInputFocus = 42 | |||
| 44 | + | x11OpKillClient = 113 | |||
| 45 | + | ) | |||
| 46 | + | ||||
| 47 | + | func startX11Peer(t *testing.T) (int, *x11Peer) { | |||
| 48 | + | t.Helper() | |||
| 49 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 50 | + | if err != nil { | |||
| 51 | + | t.Fatal(err) | |||
| 52 | + | } | |||
| 53 | + | p := &x11Peer{ | |||
| 54 | + | t: t, f: os.NewFile(uintptr(fds[1]), "x11peer"), atoms: map[string]uint32{}, nextAtom: 1000, | |||
| 55 | + | props: map[uint32]map[uint32][]byte{}, propMeta: map[uint32]map[uint32][2]uint32{}, done: make(chan struct{}), | |||
| 56 | + | } | |||
| 57 | + | t.Cleanup(func() { p.f.Close(); <-p.done }) | |||
| 58 | + | go p.serve() | |||
| 59 | + | return fds[0], p | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func (p *x11Peer) setupReply() []byte { | |||
| 63 | + | vendor := []byte("x11pr\x00\x00\x00") | |||
| 64 | + | b := make([]byte, 40+len(vendor)+40) | |||
| 65 | + | b[0], b[2] = 1, 11 | |||
| 66 | + | binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4)) | |||
| 67 | + | binary.LittleEndian.PutUint32(b[12:], 0x200000) | |||
| 68 | + | binary.LittleEndian.PutUint32(b[16:], 0x1fffff) | |||
| 69 | + | binary.LittleEndian.PutUint16(b[24:], 5) | |||
| 70 | + | b[28] = 1 | |||
| 71 | + | copy(b[40:], vendor) | |||
| 72 | + | scr := b[40+len(vendor):] | |||
| 73 | + | binary.LittleEndian.PutUint32(scr, x11PeerRoot) | |||
| 74 | + | binary.LittleEndian.PutUint32(scr[32:], 0x22) | |||
| 75 | + | scr[38] = 24 | |||
| 76 | + | return b | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func (p *x11Peer) serve() { | |||
| 80 | + | defer close(p.done) | |||
| 81 | + | var hdr [12]byte | |||
| 82 | + | if _, err := io.ReadFull(p.f, hdr[:]); err != nil { | |||
| 83 | + | return | |||
| 84 | + | } | |||
| 85 | + | if _, err := p.f.Write(p.setupReply()); err != nil { | |||
| 86 | + | return | |||
| 87 | + | } | |||
| 88 | + | for { | |||
| 89 | + | var h [4]byte | |||
| 90 | + | if _, err := io.ReadFull(p.f, h[:]); err != nil { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | raw := h[:] | |||
| 94 | + | n := int(binary.LittleEndian.Uint16(h[2:])) * 4 | |||
| 95 | + | if n == 0 { | |||
| 96 | + | var l [4]byte | |||
| 97 | + | if _, err := io.ReadFull(p.f, l[:]); err != nil { | |||
| 98 | + | return | |||
| 99 | + | } | |||
| 100 | + | raw = append(raw, l[:]...) | |||
| 101 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |||
| 102 | + | } | |||
| 103 | + | rest := make([]byte, n-len(raw)) | |||
| 104 | + | if _, err := io.ReadFull(p.f, rest); err != nil { | |||
| 105 | + | return | |||
| 106 | + | } | |||
| 107 | + | raw = append(append([]byte(nil), raw...), rest...) | |||
| 108 | + | p.mu.Lock() | |||
| 109 | + | p.seq++ | |||
| 110 | + | r := x11Req{h[0], h[1], raw} | |||
| 111 | + | p.reqs = append(p.reqs, r) | |||
| 112 | + | p.answer(r, raw[len(raw)-len(rest):]) | |||
| 113 | + | p.mu.Unlock() | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func (p *x11Peer) reply(data uint8, body []byte) { | |||
| 118 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |||
| 119 | + | b := make([]byte, 32+4*words) | |||
| 120 | + | b[0], b[1] = 1, data | |||
| 121 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 122 | + | binary.LittleEndian.PutUint32(b[4:], uint32(words)) | |||
| 123 | + | copy(b[8:], body) | |||
| 124 | + | _, _ = p.f.Write(b) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func x11le32(v ...uint32) []byte { | |||
| 128 | + | b := make([]byte, 0, 4*len(v)) | |||
| 129 | + | for _, x := range v { | |||
| 130 | + | b = binary.LittleEndian.AppendUint32(b, x) | |||
| 131 | + | } | |||
| 132 | + | return b | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | var x11Exts = map[string]uint8{"BIG-REQUESTS": 133, "XC-MISC": 136, "Composite": 142} | |||
| 136 | + | ||||
| 137 | + | func (p *x11Peer) answer(r x11Req, body []byte) { | |||
| 138 | + | switch { | |||
| 139 | + | case r.major == 98: // QueryExtension | |||
| 140 | + | major, ok := x11Exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |||
| 141 | + | present := uint8(0) | |||
| 142 | + | if ok { | |||
| 143 | + | present = 1 | |||
| 144 | + | } | |||
| 145 | + | p.reply(0, []byte{present, major, 0, 0}) | |||
| 146 | + | case r.major == 16: // InternAtom | |||
| 147 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |||
| 148 | + | a, ok := p.atoms[name] | |||
| 149 | + | if !ok { | |||
| 150 | + | p.nextAtom++ | |||
| 151 | + | a = p.nextAtom | |||
| 152 | + | p.atoms[name] = a | |||
| 153 | + | } | |||
| 154 | + | p.reply(0, x11le32(a)) | |||
| 155 | + | case r.major == 43: // GetInputFocus (Sync) | |||
| 156 | + | p.reply(1, x11le32(0)) | |||
| 157 | + | case r.major == 15: // QueryTree | |||
| 158 | + | b := make([]byte, 24) | |||
| 159 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |||
| 160 | + | p.reply(0, b) | |||
| 161 | + | case r.major == 14: // GetGeometry | |||
| 162 | + | b := make([]byte, 24) | |||
| 163 | + | binary.LittleEndian.PutUint32(b, x11PeerRoot) | |||
| 164 | + | binary.LittleEndian.PutUint16(b[8:], 300) | |||
| 165 | + | binary.LittleEndian.PutUint16(b[10:], 200) | |||
| 166 | + | p.reply(24, b) | |||
| 167 | + | case r.major == 18: // ChangeProperty | |||
| 168 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 169 | + | format, n := body[12], int(binary.LittleEndian.Uint32(body[16:])) | |||
| 170 | + | p.store(win, prop, binary.LittleEndian.Uint32(body[8:]), uint32(format), body[20:20+n*int(format)/8]) | |||
| 171 | + | case r.major == 20: // GetProperty | |||
| 172 | + | win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:]) | |||
| 173 | + | data, ok := p.props[win][prop] | |||
| 174 | + | b := make([]byte, 24+(len(data)+3)&^3) | |||
| 175 | + | var format uint8 | |||
| 176 | + | if ok { | |||
| 177 | + | meta := p.propMeta[win][prop] | |||
| 178 | + | format = uint8(meta[1]) | |||
| 179 | + | binary.LittleEndian.PutUint32(b[0:], meta[0]) | |||
| 180 | + | binary.LittleEndian.PutUint32(b[8:], uint32(len(data)/(int(format)/8))) | |||
| 181 | + | copy(b[24:], data) | |||
| 182 | + | } | |||
| 183 | + | p.reply(format, b) | |||
| 184 | + | case r.major >= 128: | |||
| 185 | + | switch r.major { | |||
| 186 | + | case 133: | |||
| 187 | + | p.reply(0, x11le32(0x100000)) | |||
| 188 | + | case 136: | |||
| 189 | + | if r.minor == 1 { | |||
| 190 | + | p.reply(0, x11le32(0, 1)) | |||
| 191 | + | } | |||
| 192 | + | case 142: | |||
| 193 | + | if r.minor == 0 { | |||
| 194 | + | p.reply(0, x11le32(0, 4)) | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | } | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | func (p *x11Peer) store(win, prop, typ, format uint32, data []byte) { | |||
| 201 | + | if p.props[win] == nil { | |||
| 202 | + | p.props[win], p.propMeta[win] = map[uint32][]byte{}, map[uint32][2]uint32{} | |||
| 203 | + | } | |||
| 204 | + | p.props[win][prop] = append([]byte(nil), data...) | |||
| 205 | + | p.propMeta[win][prop] = [2]uint32{typ, format} | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | // atom interns name the way the XWM would have. | |||
| 209 | + | func (p *x11Peer) atom(name string) uint32 { | |||
| 210 | + | p.mu.Lock() | |||
| 211 | + | defer p.mu.Unlock() | |||
| 212 | + | a, ok := p.atoms[name] | |||
| 213 | + | if !ok { | |||
| 214 | + | p.nextAtom++ | |||
| 215 | + | a = p.nextAtom | |||
| 216 | + | p.atoms[name] = a | |||
| 217 | + | } | |||
| 218 | + | return a | |||
| 219 | + | } | |||
| 220 | + | ||||
| 221 | + | // setProp scripts a window property (typ is an atom number, format 8 or 32). | |||
| 222 | + | func (p *x11Peer) setProp(win, prop, typ, format uint32, data []byte) { | |||
| 223 | + | p.mu.Lock() | |||
| 224 | + | defer p.mu.Unlock() | |||
| 225 | + | p.store(win, prop, typ, format, data) | |||
| 226 | + | } | |||
| 227 | + | ||||
| 228 | + | func (p *x11Peer) event(code uint8, fill func(b *[32]byte)) { | |||
| 229 | + | p.mu.Lock() | |||
| 230 | + | defer p.mu.Unlock() | |||
| 231 | + | var b [32]byte | |||
| 232 | + | b[0] = code | |||
| 233 | + | binary.LittleEndian.PutUint16(b[2:], p.seq) | |||
| 234 | + | fill(&b) | |||
| 235 | + | _, _ = p.f.Write(b[:]) | |||
| 236 | + | } | |||
| 237 | + | ||||
| 238 | + | func (p *x11Peer) createNotify(win uint32) { | |||
| 239 | + | p.event(16, func(b *[32]byte) { | |||
| 240 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 241 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 242 | + | binary.LittleEndian.PutUint16(b[16:], 100) | |||
| 243 | + | binary.LittleEndian.PutUint16(b[18:], 50) | |||
| 244 | + | }) | |||
| 245 | + | } | |||
| 246 | + | ||||
| 247 | + | func (p *x11Peer) windowEvent(code uint8, win uint32) { | |||
| 248 | + | p.event(code, func(b *[32]byte) { | |||
| 249 | + | binary.LittleEndian.PutUint32(b[4:], x11PeerRoot) | |||
| 250 | + | binary.LittleEndian.PutUint32(b[8:], win) | |||
| 251 | + | }) | |||
| 252 | + | } | |||
| 253 | + | ||||
| 254 | + | func (p *x11Peer) mapRequest(win uint32) { p.windowEvent(20, win) } | |||
| 255 | + | func (p *x11Peer) unmapNotify(win uint32) { p.windowEvent(18, win) } | |||
| 256 | + | func (p *x11Peer) destroyNotify(win uint32) { p.windowEvent(17, win) } | |||
| 257 | + | ||||
| 258 | + | func (p *x11Peer) propertyNotify(win, atom uint32) { | |||
| 259 | + | p.event(28, func(b *[32]byte) { | |||
| 260 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 261 | + | binary.LittleEndian.PutUint32(b[8:], atom) | |||
| 262 | + | }) | |||
| 263 | + | } | |||
| 264 | + | ||||
| 265 | + | func (p *x11Peer) clientMessage(win uint32, typ string, data ...uint32) { | |||
| 266 | + | t := p.atom(typ) | |||
| 267 | + | p.event(33, func(b *[32]byte) { | |||
| 268 | + | b[1] = 32 | |||
| 269 | + | binary.LittleEndian.PutUint32(b[4:], win) | |||
| 270 | + | binary.LittleEndian.PutUint32(b[8:], t) | |||
| 271 | + | for i, d := range data { | |||
| 272 | + | binary.LittleEndian.PutUint32(b[12+4*i:], d) | |||
| 273 | + | } | |||
| 274 | + | }) | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | func (p *x11Peer) requests() []x11Req { | |||
| 278 | + | p.mu.Lock() | |||
| 279 | + | defer p.mu.Unlock() | |||
| 280 | + | return append([]x11Req(nil), p.reqs...) | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | // waitReq waits for a request after the first skip ones that satisfies match. | |||
| 284 | + | func (p *x11Peer) waitReq(skip int, match func(x11Req) bool) (x11Req, int) { | |||
| 285 | + | p.t.Helper() | |||
| 286 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 287 | + | for time.Now().Before(deadline) { | |||
| 288 | + | reqs := p.requests() | |||
| 289 | + | for i := skip; i < len(reqs); i++ { | |||
| 290 | + | if match(reqs[i]) { | |||
| 291 | + | return reqs[i], i + 1 | |||
| 292 | + | } | |||
| 293 | + | } | |||
| 294 | + | time.Sleep(time.Millisecond) | |||
| 295 | + | } | |||
| 296 | + | p.t.Fatal("x11 peer: expected request never arrived") | |||
| 297 | + | return x11Req{}, 0 | |||
| 298 | + | } | |||
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 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | 129 | // window is an xdg_toplevel, or an xdg_popup when popup is set. | ||
| 130 | type window struct { | 130 | type window struct { | ||
| 131 | - | id ports.WindowID | 131 | + | id ports.WindowID |
| 132 | - | toplevel *xdgshell.Toplevel | 132 | + | toplevel *xdgshell.Toplevel |
| 133 | - | popup *popup | 133 | + | popup *popup |
| 134 | - | xdg *xdgSurface | 134 | + | xdg *xdgSurface |
| 135 | + | // x11 is set for the window of an X11 client (x11window.go). | |||
| 136 | + | x11 *x11Window | |||
| 135 | appID, title string | 137 | appID, title string | ||
| 136 | mapped bool | 138 | mapped bool | ||
| 137 | // gen counts the mappings: an unmap and a remap is a new toplevel to | 139 | // gen counts the mappings: an unmap and a remap is a new toplevel to | ||
| @@ -151,8 +153,8 @@ type window struct { | |||||
| 151 | hasLast bool | 153 | hasLast bool | ||
| 152 | } | 154 | } | ||
| 153 | 155 | ||||
| 154 | - | // sendConfigure sends w.last as xdg_toplevel + xdg_surface configure. | 156 | + | // sendXDGConfigure sends w.last as xdg_toplevel + xdg_surface configure. |
| 155 | - | func (w *window) sendConfigure() { | 157 | + | func (w *window) sendXDGConfigure() { |
| 156 | c := w.last | 158 | c := w.last | ||
| 157 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version())) | 159 | w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), toplevelStates(c, w.toplevel.Version())) | ||
| 158 | s := w.xdg.server | 160 | s := w.xdg.server | ||
| @@ -192,24 +194,29 @@ func toplevelStates(c ports.ConfigureWindow, version int32) []byte { | |||||
| 192 | } | 194 | } | ||
| 193 | 195 | ||||
| 194 | func (w *window) unmap() { | 196 | func (w *window) unmap() { | ||
| 195 | - | if w.xdg.server.seat.pointerFocus == w.id { | 197 | + | s := w.srv() |
| 196 | - | w.xdg.server.changePointerFocus(0, 0, 0) | 198 | + | if s.seat.pointerFocus == w.id { |
| 199 | + | s.changePointerFocus(0, 0, 0) | |||
| 197 | } | 200 | } | ||
| 198 | - | if w.xdg.server.seat.focused == w.id { | 201 | + | if s.seat.focused == w.id { |
| 199 | - | w.xdg.server.changeFocus(0) | 202 | + | s.changeFocus(0) |
| 200 | } | 203 | } | ||
| 201 | if !w.mapped { | 204 | if !w.mapped { | ||
| 202 | return | 205 | return | ||
| 203 | } | 206 | } | ||
| 204 | w.mapped = false | 207 | w.mapped = false | ||
| 205 | - | w.xdg.server.toplevelClosed(w.id) | 208 | + | s.toplevelClosed(w.id) |
| 206 | - | w.xdg.surface.resetInputEmission() | 209 | + | if surf := w.surf(); surf != nil { |
| 207 | - | w.xdg.configured = false | 210 | + | surf.resetInputEmission() |
| 208 | - | w.xdg.acked = false | 211 | + | } |
| 209 | - | w.xdg.serials = nil | 212 | + | if x := w.xdg; x != nil { |
| 210 | - | w.xdg.server.emit(ports.WindowUnmapped{ID: w.id}) | 213 | + | x.configured = false |
| 211 | - | w.xdg.server.syncInhibitors() | 214 | + | x.acked = false |
| 212 | - | w.xdg.server.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | 215 | + | x.serials = nil |
| 216 | + | } | |||
| 217 | + | s.emit(ports.WindowUnmapped{ID: w.id}) | |||
| 218 | + | s.syncInhibitors() | |||
| 219 | + | s.emitContent(ports.SurfaceContent{ID: w.id}, damage{full: true}) | |||
| 213 | } | 220 | } | ||
| 214 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | 221 | func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) { | ||
| 215 | if x.window != nil { | 222 | if x.window != nil { | ||
| @@ -337,7 +344,7 @@ func (w *window) setParent(p *window) { | |||||
| 337 | if p != nil && p.mapped { | 344 | if p != nil && p.mapped { | ||
| 338 | id = p.id | 345 | id = p.id | ||
| 339 | } | 346 | } | ||
| 340 | - | w.xdg.server.emit(ports.WindowParent{ID: w.id, Parent: id}) | 347 | + | w.srv().emit(ports.WindowParent{ID: w.id, Parent: id}) |
| 341 | } | 348 | } | ||
| 342 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | 349 | func (t top) SetTitle(_ *xdgshell.Toplevel, title string) { | ||
| 343 | if title == t.w.title { | 350 | if title == t.w.title { | ||
| @@ -417,7 +424,7 @@ func (w *window) afterCommit() { | |||||
| 417 | } | 424 | } | ||
| 418 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | 425 | if fw, fh := w.size(); fw > 0 && fh > 0 && (fw != w.floatW || fh != w.floatH) { | ||
| 419 | w.floatW, w.floatH = fw, fh | 426 | w.floatW, w.floatH = fw, fh | ||
| 420 | - | w.xdg.server.emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) | 427 | + | w.srv().emit(ports.WindowResized{ID: w.id, Width: fw, Height: fh}) |
| 421 | } | 428 | } | ||
| 422 | } | 429 | } | ||
| 423 | 430 | ||||
| @@ -431,6 +438,10 @@ func (w *window) floats() bool { | |||||
| 431 | 438 | ||||
| 432 | // size is the window geometry size, else the surface size, in logical pixels. | 439 | // size is the window geometry size, else the surface size, in logical pixels. | ||
| 433 | func (w *window) size() (int, int) { | 440 | func (w *window) size() (int, int) { | ||
| 441 | + | if w.xdg == nil { | |||
| 442 | + | c := w.surf().content | |||
| 443 | + | return c.LogicalW, c.LogicalH | |||
| 444 | + | } | |||
| 434 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | 445 | if g := w.xdg.geometry; g.W > 0 && g.H > 0 { | ||
| 435 | return g.W, g.H | 446 | return g.W, g.H | ||
| 436 | } | 447 | } | ||
| @@ -441,7 +452,7 @@ func (w *window) size() (int, int) { | |||||
| 441 | // surfacePoint turns window coordinates (core's, from the geometry origin) | 452 | // surfacePoint turns window coordinates (core's, from the geometry origin) | ||
| 442 | // into surface coordinates. | 453 | // into surface coordinates. | ||
| 443 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | 454 | func (w *window) surfacePoint(x, y float64) (float64, float64) { | ||
| 444 | - | g := w.xdg.geometry | 455 | + | g := w.geometry() |
| 445 | return x + float64(g.X), y + float64(g.Y) | 456 | return x + float64(g.X), y + float64(g.Y) | ||
| 446 | } | 457 | } | ||
| 447 | 458 | ||||
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 | 1 | + | // Package xwayland starts an X11 backend on demand. NeferWL owns the display |
| 2 | - | // demand: neferwl owns the X11 display sockets and starts the satellite | 2 | + | // sockets and starts the selected backend when the first client connects. |
| 3 | - | // when the first X11 client connects, and again after it exits. | |||
| 4 | package xwayland | 3 | package xwayland | ||
| 5 | 4 | ||||
| 6 | import ( | 5 | import ( | ||
| @@ -17,15 +16,26 @@ import ( | |||||
| 17 | "syscall" | 16 | "syscall" | ||
| 18 | "time" | 17 | "time" | ||
| 19 | 18 | ||||
| 19 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 20 | "github.com/bnema/zerowrap" | 20 | "github.com/bnema/zerowrap" | ||
| 21 | "golang.org/x/sys/unix" | 21 | "golang.org/x/sys/unix" | ||
| 22 | ) | 22 | ) | ||
| 23 | 23 | ||||
| 24 | // Options configures the X11 display. | 24 | // Options configures the X11 display. | ||
| 25 | type Options struct { | 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 | Binary string | 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 | Env []string | 39 | Env []string | ||
| 30 | // Dir holds the X11 sockets and TmpDir the lock files: /tmp/.X11-unix | 40 | // Dir holds the X11 sockets and TmpDir the lock files: /tmp/.X11-unix | ||
| 31 | // and /tmp outside tests. | 41 | // and /tmp outside tests. | ||
| @@ -35,8 +45,9 @@ type Options struct { | |||||
| 35 | Log zerowrap.Logger | 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 | type Display struct { | 49 | type Display struct { | ||
| 50 | + | starter starter | |||
| 40 | opts Options | 51 | opts Options | ||
| 41 | n int | 52 | n int | ||
| 42 | sockets []*os.File // unix socket, then the abstract one | 53 | sockets []*os.File // unix socket, then the abstract one | ||
| @@ -81,7 +92,10 @@ func Open(opts Options) (*Display, error) { | |||||
| 81 | _ = os.Remove(lock) | 92 | _ = os.Remove(lock) | ||
| 82 | return nil, fmt.Errorf("write X11 lock: %w", err) | 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 | if err := d.listen(); err != nil { | 99 | if err := d.listen(); err != nil { | ||
| 86 | last = err | 100 | last = err | ||
| 87 | d.Close() | 101 | d.Close() | ||
| @@ -212,15 +226,15 @@ func (d *Display) Run(ctx context.Context) error { | |||||
| 212 | return err | 226 | return err | ||
| 213 | } | 227 | } | ||
| 214 | start := time.Now() | 228 | start := time.Now() | ||
| 215 | - | stderr := &logWriter{log: d.opts.Log} | 229 | + | stderr := &logWriter{log: d.opts.Log, backend: d.backendName()} |
| 216 | - | err := d.serve(ctx, stderr) | 230 | + | err := d.starter.start(ctx, d, stderr) |
| 217 | if ctx.Err() != nil { | 231 | if ctx.Err() != nil { | ||
| 218 | return nil | 232 | return nil | ||
| 219 | } | 233 | } | ||
| 220 | if err != nil { | 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 | } else { | 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 | // Clients the satellite never accepted would wake us at once: a | 239 | // Clients the satellite never accepted would wake us at once: a | ||
| 226 | // satellite that cannot start must not respawn in a loop. | 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 | return ctx.Err() | 275 | return ctx.Err() | ||
| 262 | } | 276 | } | ||
| 263 | 277 | ||||
| 264 | - | // serve runs the satellite on the sockets until it exits or ctx ends. | 278 | + | // starter runs a backend on the reserved sockets until it exits or ctx ends. |
| 265 | - | func (d *Display) serve(ctx context.Context, stderr *logWriter) error { | 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 | args := []string{d.Name()} | 286 | args := []string{d.Name()} | ||
| 267 | for i := range d.sockets { | 287 | for i := range d.sockets { | ||
| 268 | args = append(args, "-listenfd", strconv.Itoa(3+i)) | 288 | args = append(args, "-listenfd", strconv.Itoa(3+i)) | ||
| @@ -318,10 +338,11 @@ func (d *Display) dropPending() { | |||||
| 318 | // logWriter logs the satellite's stderr line by line and keeps the last | 338 | // logWriter logs the satellite's stderr line by line and keeps the last | ||
| 319 | // lines for the exit log; exec copies it from a pipe on its own goroutine. | 339 | // lines for the exit log; exec copies it from a pipe on its own goroutine. | ||
| 320 | type logWriter struct { | 340 | type logWriter struct { | ||
| 321 | - | mu sync.Mutex | 341 | + | backend string |
| 322 | - | log zerowrap.Logger | 342 | + | mu sync.Mutex |
| 323 | - | line []byte | 343 | + | log zerowrap.Logger |
| 324 | - | last []string | 344 | + | line []byte |
| 345 | + | last []string | |||
| 325 | } | 346 | } | ||
| 326 | 347 | ||||
| 327 | // tail is the last lines written. | 348 | // tail is the last lines written. | ||
| @@ -342,7 +363,7 @@ func (w *logWriter) Write(p []byte) (int, error) { | |||||
| 342 | } | 363 | } | ||
| 343 | if i > 0 { | 364 | if i > 0 { | ||
| 344 | line := string(w.line[:i]) | 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 | if w.last = append(w.last, line); len(w.last) > 5 { | 367 | if w.last = append(w.last, line); len(w.last) > 5 { | ||
| 347 | w.last = w.last[1:] | 368 | w.last = w.last[1:] | ||
| 348 | } | 369 | } | ||
| @@ -354,3 +375,10 @@ func (w *logWriter) Write(p []byte) (int, error) { | |||||
| 354 | } | 375 | } | ||
| 355 | return len(p), nil | 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 | "time" | 11 | "time" | ||
| 12 | 12 | ||||
| 13 | "github.com/bnema/neferwl/internal/adapters/logging" | 13 | "github.com/bnema/neferwl/internal/adapters/logging" | ||
| 14 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 15 | + | "github.com/stretchr/testify/mock" | |||
| 14 | "golang.org/x/sys/unix" | 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 | "context" | 5 | "context" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "io" | 7 | "io" | ||
| 8 | + | "os/exec" | |||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| 10 | "github.com/bnema/neferwl/internal/adapters/logging" | 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 | // nil when X11 is off or unavailable: the session runs without it. | 193 | // nil when X11 is off or unavailable: the session runs without it. | ||
| 193 | - | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger) *xwayland.Display { | 194 | + | func openXwayland(ctx context.Context, opts Options, env []string, log zerowrap.Logger, attach func(int), attachWM func(context.Context, int) func()) *xwayland.Display { |
| 194 | - | bin := opts.Config.Xwayland | 195 | + | cfg := opts.Config.Xwayland |
| 195 | - | if bin == "" || opts.NoXwayland { | 196 | + | bin := cfg.Binary |
| 197 | + | if cfg.Backend == ports.XwaylandOff || opts.NoXwayland { | |||
| 196 | return nil | 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 | log.Warn().Str("binary", bin).Msg("xwayland-satellite 0.7 or later not found: X11 apps disabled") | 206 | log.Warn().Str("binary", bin).Msg("xwayland-satellite 0.7 or later not found: X11 apps disabled") | ||
| 200 | return nil | 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 | if err != nil { | 210 | if err != nil { | ||
| 204 | log.Warn().Err(err).Msg("X11 display disabled") | 211 | log.Warn().Err(err).Msg("X11 display disabled") | ||
| 205 | return nil | 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 | s.statePath = statePath | 152 | s.statePath = statePath | ||
| 153 | s.childEnv = append(s.childEnv, statefile.Env+"="+statePath) | 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 | s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name()) | 156 | s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name()) | ||
| 157 | } | 157 | } | ||
| 158 | s.child = launcher.New(s.childEnv, s.security, logging.For(ctx, "launcher")) | 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 | binds map[binding]Action | 103 | binds map[binding]Action | ||
| 104 | pressed map[string]bool | 104 | pressed map[string]bool | ||
| 105 | configures configures | 105 | configures configures | ||
| 106 | - | pointer WindowID | 106 | + | // placed is the last position (X, Y of the Rect) sent in a PlaceWindow, per window. |
| 107 | - | grab WindowID | 107 | + | placed map[WindowID]Rect |
| 108 | - | buttons map[uint32]bool | 108 | + | pointer WindowID |
| 109 | + | grab WindowID | |||
| 110 | + | buttons map[uint32]bool | |||
| 109 | // drag is the pointer drag in progress (drag.go); swallow holds the | 111 | // drag is the pointer drag in progress (drag.go); swallow holds the | ||
| 110 | // buttons whose release no client must see. mods are the modifiers | 112 | // buttons whose release no client must see. mods are the modifiers | ||
| 111 | // held, cmdMod the one keyboard.cmd names. lastButton is the latest | 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 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | 419 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | ||
| 418 | } | 420 | } | ||
| 419 | // A placeholder screen holds windows until the first output arrives. | 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 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | 423 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | ||
| 422 | if err := c.apply(cfg); err != nil { | 424 | if err := c.apply(cfg); err != nil { | ||
| 423 | return nil, err | 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 | for { | 38 | for { | ||
| 39 | switch v := receive(t, ch); v.(type) { | 39 | switch v := receive(t, ch); v.(type) { | ||
| 40 | case nil, ports.SetOutputs, ports.UserActivity: | 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 | default: | 45 | default: | ||
| 42 | return v | 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 | c.configures.mark(v) | 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 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | 130 | scene.Windows = append(scene.Windows, c.scenePopups(sc)...) | ||
| 121 | if icon, ok := c.dataDragIconOn(i, sc); ok { | 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 | // The window is unmapped: a toplevel that maps again keeps its ID and | 453 | // The window is unmapped: a toplevel that maps again keeps its ID and | ||
| 454 | // must be configured from nothing (a leaving entry keeps no configure). | 454 | // must be configured from nothing (a leaving entry keeps no configure). | ||
| 455 | c.configures.forget(v.ID) | 455 | c.configures.forget(v.ID) | ||
| 456 | + | delete(c.placed, v.ID) | |||
| 456 | sc, w := c.screenOf(v.ID) | 457 | sc, w := c.screenOf(v.ID) | ||
| 457 | if sc == nil { | 458 | if sc == nil { | ||
| 458 | return | 459 | return | ||
M internal/ports/client_commands.go +10 -0
| @@ -47,6 +47,16 @@ type ConfigureWindow struct { | |||||
| 47 | 47 | ||||
| 48 | func (ConfigureWindow) clientCommand() {} | 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 | // OutputPlacement is one output in the global layout. X, Y, Width and | 60 | // OutputPlacement is one output in the global layout. X, Y, Width and | ||
| 51 | // Height are logical (after Transform: they swap for 90/270); | 61 | // Height are logical (after Transform: they swap for 90/270); | ||
| 52 | // physical = logical × Scale. Info.Width and Info.Height are the physical | 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 | // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over | 20 | // xdg-foreign), 0 when none. A dialog of a fullscreen window shows over | ||
| 21 | // it. | 21 | // it. | ||
| 22 | Parent WindowID | 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 | // Remap is set when the toplevel mapped before and was unmapped (a tray | 26 | // Remap is set when the toplevel mapped before and was unmapped (a tray | ||
| 24 | // app restored): window rules only place its first map. | 27 | // app restored): window rules only place its first map. | ||
| 25 | Remap bool | 28 | Remap bool | ||
M internal/ports/config.go +17 -3
| @@ -101,9 +101,8 @@ type Config struct { | |||||
| 101 | Command []string | 101 | Command []string | ||
| 102 | AutoOpen string | 102 | AutoOpen string | ||
| 103 | } | 103 | } | ||
| 104 | - | // Xwayland is the xwayland-satellite binary serving X11 clients; empty | 104 | + | // Xwayland selects the X11 launcher. |
| 105 | - | // disables X11. | 105 | + | Xwayland XwaylandConfig |
| 106 | - | Xwayland string | |||
| 107 | // Startup are commands run once when the session starts, in order. | 106 | // Startup are commands run once when the session starts, in order. | ||
| 108 | // One startup line may hold several, split by SplitCommands. | 107 | // One startup line may hold several, split by SplitCommands. | ||
| 109 | Startup [][]string | 108 | Startup [][]string | ||
| @@ -298,3 +297,18 @@ type WindowRule struct { | |||||
| 298 | // (1 to 16; 0 leaves it automatic). | 297 | // (1 to 16; 0 leaves it automatic). | ||
| 299 | Cells int | 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 | + | } | |||