diff options
author | David Edmundson <davidedmundson@kde.org> | 2022-01-28 16:24:32 +0000 |
---|---|---|
committer | David Edmundson <davidedmundson@kde.org> | 2023-03-07 13:26:50 +0000 |
commit | 26d8603b523af973682b7e602f1158ae62f21c9b (patch) | |
tree | cf5223fea624e961af7150fdde44bdf1e9cfa27a /tests | |
parent | 83599e16789f945ef15954f2a2aabb37b245e596 (diff) | |
download | qtwayland-26d8603b523af973682b7e602f1158ae62f21c9b.tar.gz |
Introduce path for surviving compositor restarts
This patch introduces an optional mechanism for clients to survive a
crash and reconnect seemingly seamlessly.
In the event of a disconnect from the compositor socket we simply try to
reconnect again and replay any data needed so that we maintain a
consistent state to where we left off.
From an application point-of-view any open popups will be dismissed and
we we potentially get a new framecallback, but it will be almost
entirely transparent. Users of custom QWaylandClientExtensions will be
notified via the activeChanged signal and rebuild as though the
compositor had withdrawn and re-announced the global.
OpenGL contexts will be marked as invalid, and handled the same way as a
GPU reset. On the next frame RHI will notice these are invalid and
recreate them, only now against a new wl_display and new EGLDisplay.
Users of low level EGL/native objects might be affected, but the
alternative at this point is being closed anyway. The entire codepath is
only activated via an environment variable.
Change-Id: I6c4acc885540e14cead7640794df86dd974fef4f
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
Diffstat (limited to 'tests')
-rw-r--r-- | tests/auto/client/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/client/reconnect/CMakeLists.txt | 11 | ||||
-rw-r--r-- | tests/auto/client/reconnect/tst_reconnect.cpp | 210 | ||||
-rw-r--r-- | tests/auto/client/reconnect/wl-socket.c | 166 | ||||
-rw-r--r-- | tests/auto/client/reconnect/wl-socket.h | 34 | ||||
-rw-r--r-- | tests/auto/client/shared/corecompositor.cpp | 12 | ||||
-rw-r--r-- | tests/auto/client/shared/corecompositor.h | 4 | ||||
-rw-r--r-- | tests/auto/client/shared/mockcompositor.cpp | 4 | ||||
-rw-r--r-- | tests/auto/client/shared/mockcompositor.h | 2 |
9 files changed, 436 insertions, 8 deletions
diff --git a/tests/auto/client/CMakeLists.txt b/tests/auto/client/CMakeLists.txt index 44cf3271..5ae005ea 100644 --- a/tests/auto/client/CMakeLists.txt +++ b/tests/auto/client/CMakeLists.txt @@ -16,6 +16,7 @@ if (NOT WEBOS) add_subdirectory(nooutput) add_subdirectory(output) add_subdirectory(primaryselectionv1) + add_subdirectory(reconnect) add_subdirectory(seatv4) add_subdirectory(seatv7) add_subdirectory(seat) diff --git a/tests/auto/client/reconnect/CMakeLists.txt b/tests/auto/client/reconnect/CMakeLists.txt new file mode 100644 index 00000000..21ce68fd --- /dev/null +++ b/tests/auto/client/reconnect/CMakeLists.txt @@ -0,0 +1,11 @@ +##################################################################### +## tst_client Test: +##################################################################### + +qt_internal_add_test(tst_reconnect + SOURCES + wl-socket.c + tst_reconnect.cpp + PUBLIC_LIBRARIES + SharedClientTest + ) diff --git a/tests/auto/client/reconnect/tst_reconnect.cpp b/tests/auto/client/reconnect/tst_reconnect.cpp new file mode 100644 index 00000000..93007d4c --- /dev/null +++ b/tests/auto/client/reconnect/tst_reconnect.cpp @@ -0,0 +1,210 @@ +// Copyright (C) 2023 David Edmundson <davidedmundson@kde.org> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "mockcompositor.h" + +#include <QBackingStore> +#include <QPainter> +#include <QScreen> +#include <QWindow> +#include <QMimeData> +#include <QPixmap> +#include <QDrag> +#include <QWindow> +#if QT_CONFIG(opengl) +#include <QOpenGLWindow> +#endif +#include <QRasterWindow> + +#include <QtTest/QtTest> +#include <QtWaylandClient/private/qwaylandintegration_p.h> +#include <QtGui/private/qguiapplication_p.h> + +#include "wl-socket.h" + +using namespace MockCompositor; + +class TestWindow : public QRasterWindow +{ +public: + TestWindow() + { + } + + void focusInEvent(QFocusEvent *) override + { + ++focusInEventCount; + } + + void focusOutEvent(QFocusEvent *) override + { + ++focusOutEventCount; + } + + void keyPressEvent(QKeyEvent *event) override + { + ++keyPressEventCount; + keyCode = event->nativeScanCode(); + } + + void keyReleaseEvent(QKeyEvent *event) override + { + ++keyReleaseEventCount; + keyCode = event->nativeScanCode(); + } + + void mousePressEvent(QMouseEvent *event) override + { + ++mousePressEventCount; + mousePressPos = event->position().toPoint(); + } + + void mouseReleaseEvent(QMouseEvent *) override + { + ++mouseReleaseEventCount; + } + + void touchEvent(QTouchEvent *event) override + { + Q_UNUSED(event); + ++touchEventCount; + } + + QPoint frameOffset() const { return QPoint(frameMargins().left(), frameMargins().top()); } + + int focusInEventCount = 0; + int focusOutEventCount = 0; + int keyPressEventCount = 0; + int keyReleaseEventCount = 0; + int mousePressEventCount = 0; + int mouseReleaseEventCount = 0; + int touchEventCount = 0; + + uint keyCode = 0; + QPoint mousePressPos; +}; + +class tst_WaylandReconnect : public QObject +{ + Q_OBJECT +public: + tst_WaylandReconnect(); + void triggerReconnect(); + + template<typename function_type, typename... arg_types> + auto exec(function_type func, arg_types&&... args) -> decltype(func()) + { + return m_comp->exec(func, std::forward<arg_types>(args)...); + } + +private Q_SLOTS: +//core + void cleanup() { QTRY_VERIFY2(m_comp->isClean(), qPrintable(m_comp->dirtyMessage())); } + void basicWindow(); + +//input + void keyFocus(); + +private: + void configureWindow(); + QScopedPointer<DefaultCompositor> m_comp; + wl_socket *m_socket; +}; + +tst_WaylandReconnect::tst_WaylandReconnect() +{ + m_socket = wl_socket_create(); + QVERIFY(m_socket); + const int socketFd = wl_socket_get_fd(m_socket); + const QByteArray socketName = wl_socket_get_display_name(m_socket); + qputenv("WAYLAND_DISPLAY", socketName); + + m_comp.reset(new DefaultCompositor(CoreCompositor::Default, dup(socketFd))); +} + +void tst_WaylandReconnect::triggerReconnect() +{ + const int socketFd = wl_socket_get_fd(m_socket); + m_comp.reset(new DefaultCompositor(CoreCompositor::Default, dup(socketFd))); + QTest::qWait(50); //we need to spin the main loop to actually reconnect +} + +void tst_WaylandReconnect::basicWindow() +{ + QRasterWindow window; + window.resize(64, 48); + window.show(); + QCOMPOSITOR_TRY_VERIFY(m_comp->xdgToplevel()); + + triggerReconnect(); + + QCOMPOSITOR_TRY_VERIFY(m_comp->xdgToplevel()); +} + +void tst_WaylandReconnect::keyFocus() +{ + TestWindow window; + window.resize(64, 48); + window.show(); + + configureWindow(); + QTRY_VERIFY(window.isExposed()); + exec([=] { + m_comp->keyboard()->sendEnter(m_comp->surface()); + }); + QTRY_COMPARE(window.focusInEventCount, 1); + + uint keyCode = 80; + QCOMPARE(window.keyPressEventCount, 0); + exec([=] { + m_comp->keyboard()->sendKey(m_comp->client(), keyCode - 8, Keyboard::key_state_pressed); + }); + QTRY_COMPARE(window.keyPressEventCount, 1); + QCOMPARE(QGuiApplication::focusWindow(), &window); + + triggerReconnect(); + configureWindow(); + + // on reconnect our knowledge of focus is reset to a clean slate + QCOMPARE(QGuiApplication::focusWindow(), nullptr); + QTRY_COMPARE(window.focusOutEventCount, 1); + + // fake the user explicitly focussing this window afterwards + exec([=] { + m_comp->keyboard()->sendEnter(m_comp->surface()); + }); + exec([=] { + m_comp->keyboard()->sendKey(m_comp->client(), keyCode - 8, Keyboard::key_state_pressed); + }); + QTRY_COMPARE(window.focusInEventCount, 2); + QTRY_COMPARE(window.keyPressEventCount, 2); +} + + +void tst_WaylandReconnect::configureWindow() +{ + QCOMPOSITOR_TRY_VERIFY(m_comp->xdgToplevel()); + m_comp->exec([=] { + m_comp->xdgToplevel()->sendConfigure({0, 0}, {}); + const uint serial = m_comp->nextSerial(); // Let the window decide the size + m_comp->xdgSurface()->sendConfigure(serial); + }); +} + +int main(int argc, char **argv) +{ + // Note when debugging that a failing reconnect will exit this + // test rather than fail. Making sure it finishes is important! + + QTemporaryDir tmpRuntimeDir; + setenv("QT_QPA_PLATFORM", "wayland", 1); // force QGuiApplication to use wayland plugin + setenv("QT_WAYLAND_RECONNECT", "1", 1); + setenv("XDG_CURRENT_DESKTOP", "qtwaylandtests", 1); + + tst_WaylandReconnect tc; + QGuiApplication app(argc, argv); + QTEST_SET_MAIN_SOURCE_PATH + return QTest::qExec(&tc, argc, argv); +} + +#include "tst_reconnect.moc" diff --git a/tests/auto/client/reconnect/wl-socket.c b/tests/auto/client/reconnect/wl-socket.c new file mode 100644 index 00000000..f96de8c7 --- /dev/null +++ b/tests/auto/client/reconnect/wl-socket.c @@ -0,0 +1,166 @@ +// Copyright (C) 2023 David Edmundson <davidedmundson@kde.org> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#define _DEFAULT_SOURCE +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/file.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/un.h> +#include <unistd.h> + +/* This is the size of the char array in struct sock_addr_un. + * No Wayland socket can be created with a path longer than this, + * including the null terminator. + */ +#ifndef UNIX_PATH_MAX +#define UNIX_PATH_MAX 108 +#endif + +#define LOCK_SUFFIX ".lock" +#define LOCK_SUFFIXLEN 5 + +struct wl_socket { + int fd; + int fd_lock; + struct sockaddr_un addr; + char lock_addr[UNIX_PATH_MAX + LOCK_SUFFIXLEN]; + char display_name[16]; +}; + +static struct wl_socket *wl_socket_alloc(void) +{ + struct wl_socket *s; + + s = malloc(sizeof *s); + if (!s) + return NULL; + + s->fd = -1; + s->fd_lock = -1; + + return s; +} + +static int wl_socket_lock(struct wl_socket *socket) +{ + struct stat socket_stat; + + snprintf(socket->lock_addr, sizeof socket->lock_addr, "%s%s", socket->addr.sun_path, LOCK_SUFFIX); + + // differening from kwin, we're back to setting CLOEXEC as we're all in process + socket->fd_lock = open(socket->lock_addr, O_CREAT | O_RDWR | O_CLOEXEC, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)); + + if (socket->fd_lock < 0) { + printf("unable to open lockfile %s check permissions\n", socket->lock_addr); + goto err; + } + + if (flock(socket->fd_lock, LOCK_EX | LOCK_NB) < 0) { + printf("unable to lock lockfile %s, maybe another compositor is running\n", socket->lock_addr); + goto err_fd; + } + + if (lstat(socket->addr.sun_path, &socket_stat) < 0) { + if (errno != ENOENT) { + printf("did not manage to stat file %s\n", socket->addr.sun_path); + goto err_fd; + } + } else if (socket_stat.st_mode & S_IWUSR || socket_stat.st_mode & S_IWGRP) { + unlink(socket->addr.sun_path); + } + + return 0; +err_fd: + close(socket->fd_lock); + socket->fd_lock = -1; +err: + *socket->lock_addr = 0; + /* we did not set this value here, but without lock the + * socket won't be created anyway. This prevents the + * wl_socket_destroy from unlinking already existing socket + * created by other compositor */ + *socket->addr.sun_path = 0; + + return -1; +} + +void wl_socket_destroy(struct wl_socket *s) +{ + if (s->addr.sun_path[0]) + unlink(s->addr.sun_path); + if (s->fd >= 0) + close(s->fd); + if (s->lock_addr[0]) + unlink(s->lock_addr); + if (s->fd_lock >= 0) + close(s->fd_lock); + + free(s); +} + +const char *wl_socket_get_display_name(struct wl_socket *s) +{ + return s->display_name; +} + +int wl_socket_get_fd(struct wl_socket *s) +{ + return s->fd; +} + +struct wl_socket *wl_socket_create() +{ + struct wl_socket *s; + int displayno = 0; + int name_size; + + /* A reasonable number of maximum default sockets. If + * you need more than this, use the explicit add_socket API. */ + const int MAX_DISPLAYNO = 32; + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (!runtime_dir) { + printf("XDG_RUNTIME_DIR not set"); + return NULL; + } + + s = wl_socket_alloc(); + if (s == NULL) + return NULL; + + do { + snprintf(s->display_name, sizeof s->display_name, "wayland-%d", displayno); + s->addr.sun_family = AF_LOCAL; + name_size = snprintf(s->addr.sun_path, sizeof s->addr.sun_path, "%s/%s", runtime_dir, s->display_name) + 1; + assert(name_size > 0); + + if (name_size > (int)sizeof s->addr.sun_path) { + goto fail; + } + + if (wl_socket_lock(s) < 0) + continue; + + s->fd = socket(PF_LOCAL, SOCK_STREAM, 0); + + int size = SUN_LEN(&s->addr); + int ret = bind(s->fd, (struct sockaddr*)&s->addr, size); + if (ret < 0) { + goto fail; + } + ret = listen(s->fd, 128); + if (ret < 0) { + goto fail; + } + return s; + } while (displayno++ < MAX_DISPLAYNO); + +fail: + wl_socket_destroy(s); + return NULL; +} diff --git a/tests/auto/client/reconnect/wl-socket.h b/tests/auto/client/reconnect/wl-socket.h new file mode 100644 index 00000000..285870e0 --- /dev/null +++ b/tests/auto/client/reconnect/wl-socket.h @@ -0,0 +1,34 @@ +// Copyright (C) 2023 David Edmundson <davidedmundson@kde.org> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * Allocate and create a socket + * It is bound and accepted + */ +struct wl_socket *wl_socket_create(); + +/** + * Returns the file descriptor for the socket + */ +int wl_socket_get_fd(struct wl_socket *); + +/** + * Returns the name of the socket, i.e "wayland-0" + */ +char *wl_socket_get_display_name(struct wl_socket *); + +/** + * Cleanup resources and close the FD + */ +void wl_socket_destroy(struct wl_socket *socket); + +#ifdef __cplusplus +} +#endif diff --git a/tests/auto/client/shared/corecompositor.cpp b/tests/auto/client/shared/corecompositor.cpp index dd7311e4..9b2c7dea 100644 --- a/tests/auto/client/shared/corecompositor.cpp +++ b/tests/auto/client/shared/corecompositor.cpp @@ -6,10 +6,9 @@ namespace MockCompositor { -CoreCompositor::CoreCompositor(CompositorType t) +CoreCompositor::CoreCompositor(CompositorType t, int socketFd) : m_type(t) , m_display(wl_display_create()) - , m_socketName(wl_display_add_socket_auto(m_display)) , m_eventLoop(wl_display_get_event_loop(m_display)) // Start dispatching @@ -20,7 +19,12 @@ CoreCompositor::CoreCompositor(CompositorType t) } }) { - qputenv("WAYLAND_DISPLAY", m_socketName); + if (socketFd == -1) { + QByteArray socketName = wl_display_add_socket_auto(m_display); + qputenv("WAYLAND_DISPLAY", socketName); + } else { + wl_display_add_socket_fd(m_display, socketFd); + } m_timer.start(); Q_ASSERT(isClean()); } @@ -29,7 +33,9 @@ CoreCompositor::~CoreCompositor() { m_running = false; m_dispatchThread.join(); + wl_display_destroy_clients(m_display); wl_display_destroy(m_display); + qDebug() << "cleanup"; } bool CoreCompositor::isClean() diff --git a/tests/auto/client/shared/corecompositor.h b/tests/auto/client/shared/corecompositor.h index b7d1de78..6fd14371 100644 --- a/tests/auto/client/shared/corecompositor.h +++ b/tests/auto/client/shared/corecompositor.h @@ -29,7 +29,8 @@ public: }; CompositorType m_type = Default; - explicit CoreCompositor(CompositorType t = Default); + explicit CoreCompositor(CompositorType t = Default, int socketFd = -1); + ~CoreCompositor(); bool isClean(); QString dirtyMessage(); @@ -178,7 +179,6 @@ protected: CoreCompositor *m_compositor = nullptr; std::thread::id m_threadId; }; - QByteArray m_socketName; wl_event_loop *m_eventLoop = nullptr; bool m_running = true; QList<Global *> m_globals; diff --git a/tests/auto/client/shared/mockcompositor.cpp b/tests/auto/client/shared/mockcompositor.cpp index 71f3775a..43d417ff 100644 --- a/tests/auto/client/shared/mockcompositor.cpp +++ b/tests/auto/client/shared/mockcompositor.cpp @@ -6,8 +6,8 @@ namespace MockCompositor { -DefaultCompositor::DefaultCompositor(CompositorType t) - : CoreCompositor(t) +DefaultCompositor::DefaultCompositor(CompositorType t, int socketFd) + : CoreCompositor(t, socketFd) { { Lock l(this); diff --git a/tests/auto/client/shared/mockcompositor.h b/tests/auto/client/shared/mockcompositor.h index 6803a646..8d26361a 100644 --- a/tests/auto/client/shared/mockcompositor.h +++ b/tests/auto/client/shared/mockcompositor.h @@ -32,7 +32,7 @@ namespace MockCompositor { class DefaultCompositor : public CoreCompositor { public: - explicit DefaultCompositor(CompositorType t = CompositorType::Default); + explicit DefaultCompositor(CompositorType t = CompositorType::Default, int socketFd = -1); // Convenience functions Output *output(int i = 0) { return getAll<Output>().value(i, nullptr); } Surface *surface(int i = 0); |