From 563b23a33726289387193b85d90fb569631ee05f Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Tue, 13 Oct 2026 23:52:22 +0300 Subject: Reconnect to a restarted Wayland compositor Windows now outlive the compositor. On connection loss YASL sends DISPLAY_LOST instead of closing every window, retries the connection on a loop timer, rebuilds each toplevel with its title, size, state and GL context, and sends DISPLAY_RESTORED. CPU windows keep their pixels; GPU renderers drop Vulkan surfaces and GL objects on loss and rebuild them on restore. - Free the dead connection's objects on the next wait, after the application has released GPU objects built on them. - Report a lost connection as an event rather than a failed wait. - Refuse window and Vulkan surface creation while offline with the new YASL_ERROR_DISCONNECTED. - Close popups, cancel drags and fail pending clipboard requests on loss. - Print only protocol errors, not ordinary disconnects. - Examples survive a restart; game.c rebinds its GL context. - Add an end-to-end test that kills Weston under CPU, GL and Vulkan windows and requires them to recover. Co-Authored-By: Claude Opus 5.5 --- tests/helpers/run_reconnect.py | 67 +++++++++++++++++++++++ tests/meson.build | 5 ++ tests/wayland/test_reconnect.c | 118 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 tests/helpers/run_reconnect.py create mode 100644 tests/wayland/test_reconnect.c (limited to 'tests') diff --git a/tests/helpers/run_reconnect.py b/tests/helpers/run_reconnect.py new file mode 100644 index 0000000..e86e994 --- /dev/null +++ b/tests/helpers/run_reconnect.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Kill Weston under a running client, start a new one on the same socket, and +require the client to reconnect: run_reconnect.py .""" +import os +from pathlib import Path +import signal +import subprocess +import sys +import tempfile +import time + + +def start_weston(weston, root, env, log): + process = subprocess.Popen([weston, '--backend=headless', '--renderer=gl', '--socket=yasl-test', + '--idle-time=0', '--no-config', '--width=1024', '--height=768'], + env=env, stdout=log, stderr=log) + deadline = time.monotonic() + 12 + while not (root/'yasl-test').is_socket(): + if process.poll() is not None or time.monotonic() > deadline: + raise RuntimeError('Weston did not start') + time.sleep(0.02) + return process + + +def expect(client, word): + line = client.stdout.readline().strip() + if line != word: + raise RuntimeError(f'client printed {line!r}, expected {word!r}') + + +def main(): + weston, binary = sys.argv[1:] + signal.alarm(50) + with tempfile.TemporaryDirectory(prefix='yasl-reconnect-') as directory: + root = Path(directory) + env = os.environ.copy() + env.pop('WAYLAND_SOCKET', None) + env.update(XDG_RUNTIME_DIR=directory, WAYLAND_DISPLAY='yasl-test', LIBGL_ALWAYS_SOFTWARE='1') + lavapipe = next(Path('/usr/share/vulkan/icd.d').glob('*lvp*.json'), None) + if lavapipe: + env['VK_DRIVER_FILES'] = str(lavapipe) + with (root/'weston.log').open('w+') as log: + servers = [start_weston(weston, root, env, log)] + client = subprocess.Popen([binary], env=env, stdout=subprocess.PIPE, text=True) + try: + expect(client, 'ready') + servers[0].send_signal(signal.SIGKILL) + servers[0].wait() + expect(client, 'lost') + time.sleep(0.5) + servers.append(start_weston(weston, root, env, log)) + expect(client, 'restored') + if client.wait(timeout=15) != 0: + raise RuntimeError(f'client exited with {client.returncode}') + except BaseException: + client.kill() + log.flush(); log.seek(0); print(log.read(), file=sys.stderr) + raise + finally: + for server in servers: + if server.poll() is None: + server.terminate() + server.wait(timeout=5) + + +if __name__ == '__main__': + main() diff --git a/tests/meson.build b/tests/meson.build index 4cbf082..d61d094 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -93,6 +93,11 @@ if get_option('integration_tests') xwayland = find_program('Xwayland') test('surface-backends', python, args: [files('helpers/run_surfaces.py'), weston, xwayland, integration, hello_example, tablet_example, gl_integration, vulkan_integration, notepad_example], timeout: 120) + if has_wayland and has_egl + reconnect = executable('yasl-reconnect-test', 'wayland/test_reconnect.c', dependencies: yasl_dep, + c_args: ['-DTEST_VULKAN=' + (has_vulkan ? '1' : '0')]) + test('reconnect-wayland', python, args: [files('helpers/run_reconnect.py'), weston, reconnect], timeout: 60) + endif elif has_cocoa test('surface-cocoa', integration, args: ['cocoa'], timeout: 30) foreach unit : ['native', 'ownership', 'keyboard', 'tablet', 'cursor'] diff --git a/tests/wayland/test_reconnect.c b/tests/wayland/test_reconnect.c new file mode 100644 index 0000000..0c42043 --- /dev/null +++ b/tests/wayland/test_reconnect.c @@ -0,0 +1,118 @@ +/* Compositor restart: run by helpers/run_reconnect.py, which kills the compositor + * after "ready" and starts a new one after "lost". Windows must survive, keep + * their pixels, and accept new GL and Vulkan objects after DISPLAY_RESTORED. */ +#include +#include +#include +#if TEST_VULKAN +#include +#endif +#include +#include +#include + +#define CHECK(cond, what) do { if (!(cond)) { const char *why; yasl_get_error(&why); \ + fprintf(stderr, "reconnect: %s failed at line %d (%s)\n", what, __LINE__, why); exit(1); } } while (0) + +typedef void (*clear_color_fn)(float, float, float, float); +typedef void (*clear_fn)(unsigned); + +static yasl_surface *cpu, *gl; + +/* Waits up to timeout_ms for an event of the given type, failing on any CLOSE. */ +static yasl_event expect(enum yasl_event_type type, yasl_surface *surface, int timeout_ms) +{ + yasl_event e; + for (int waited = 0; waited < timeout_ms; waited += 50) { + CHECK(yasl_wait_event(&e, 50) >= 0, "wait"); + CHECK(e.type != YASL_EVENT_CLOSE, "keeping windows open"); + if (e.type == type && (!surface || e.surface == surface)) return e; + } + fprintf(stderr, "reconnect: timed out waiting for event %d\n", type); + exit(1); +} + +static void draw_gl(float red) +{ + CHECK(yasl_gl_make_current(gl), "binding GL"); + clear_color_fn clear_color = (clear_color_fn)yasl_gl_get_proc_address("glClearColor"); + clear_fn clear = (clear_fn)yasl_gl_get_proc_address("glClear"); + CHECK(clear_color && clear, "loading GL procedures"); + clear_color(red, 0.2f, 0.3f, 1); + clear(0x00004000u); + CHECK(yasl_gl_swap_buffers(gl), "swapping GL buffers"); +} + +#if TEST_VULKAN +static VkInstance instance; +static yasl_surface *vk; + +static VkSurfaceKHR vulkan_surface(void) +{ + VkSurfaceKHR out = VK_NULL_HANDLE; + CHECK(yasl_vulkan_create_surface(vk, instance, NULL, &out) == VK_SUCCESS, "creating Vulkan surface"); + return out; +} +#endif + +int main(void) +{ + CHECK(yasl_set_backend(YASL_BACKEND_WAYLAND), "selecting Wayland"); + cpu = yasl_surface_create("reconnect CPU", 160, 120, YASL_SURFACE_CPU); + CHECK(cpu, "creating CPU window"); + gl = yasl_gl_surface_create("reconnect GL", 160, 120, 0, NULL, NULL); + CHECK(gl, "creating GL window"); + + expect(YASL_EVENT_REFRESH, cpu, 5000); + int w, h; + uint32_t *pixels = yasl_cpu_pixels(cpu, &w, &h); + CHECK(pixels, "getting pixels"); + for (size_t i = 0; i < (size_t)w * h; i++) pixels[i] = 0xff336699u; + CHECK(yasl_cpu_present(cpu), "presenting"); + draw_gl(0.8f); + +#if TEST_VULKAN + CHECK(yasl_vulkan_init(vkGetInstanceProcAddr), "loading Vulkan"); + vk = yasl_surface_create("reconnect Vulkan", 160, 120, 0); + CHECK(vk, "creating Vulkan window"); + uint32_t count = 0; + const char *const *extensions = yasl_vulkan_instance_extensions(vk, &count); + VkInstanceCreateInfo info = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .enabledExtensionCount = count, .ppEnabledExtensionNames = extensions}; + CHECK(vkCreateInstance(&info, NULL, &instance) == VK_SUCCESS, "creating Vulkan instance"); + VkSurfaceKHR surface = vulkan_surface(); +#endif + + puts("ready"); + fflush(stdout); + + expect(YASL_EVENT_DISPLAY_LOST, NULL, 10000); +#if TEST_VULKAN + vkDestroySurfaceKHR(instance, surface, NULL); +#endif + CHECK(!yasl_surface_create("offline", 64, 64, YASL_SURFACE_CPU), "refusing windows while offline"); + CHECK(yasl_get_error(NULL) == YASL_ERROR_DISCONNECTED, "reporting the disconnect"); + puts("lost"); + fflush(stdout); + + expect(YASL_EVENT_DISPLAY_RESTORED, NULL, 15000); + expect(YASL_EVENT_REFRESH, cpu, 5000); + int rw, rh; + pixels = yasl_cpu_pixels(cpu, &rw, &rh); + CHECK(pixels && rw == w && rh == h, "keeping the window size"); + CHECK(pixels[0] == 0xff336699u && pixels[(size_t)w * h - 1] == 0xff336699u, "keeping the pixels"); + CHECK(yasl_cpu_present(cpu), "presenting after reconnect"); + draw_gl(0.1f); +#if TEST_VULKAN + surface = vulkan_surface(); + vkDestroySurfaceKHR(instance, surface, NULL); + vkDestroyInstance(instance, NULL); +#endif + + /* A second window proves the new connection accepts creation. */ + CHECK(yasl_surface_create("after reconnect", 64, 64, YASL_SURFACE_CPU), "creating a window after reconnect"); + yasl_gl_make_current(NULL); + yasl_shutdown(); + puts("restored"); + return 0; +} -- cgit v1.2.3