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 | |
parent | 83599e16789f945ef15954f2a2aabb37b245e596 (diff) |
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>
20 files changed, 648 insertions, 29 deletions
diff --git a/src/client/qwaylanddisplay.cpp b/src/client/qwaylanddisplay.cpp index 46c85a6a3..5c3431a05 100644 --- a/src/client/qwaylanddisplay.cpp +++ b/src/client/qwaylanddisplay.cpp @@ -65,18 +65,6 @@ #include <tuple> // for std::tie -static void checkWaylandError(struct wl_display *display) -{ - int ecode = wl_display_get_error(display); - if ((ecode == EPIPE || ecode == ECONNRESET)) { - // special case this to provide a nicer error - qWarning("The Wayland connection broke. Did the Wayland compositor die?"); - } else { - qWarning("The Wayland connection experienced a fatal error: %s", strerror(ecode)); - } - _exit(1); -} - QT_BEGIN_NAMESPACE namespace QtWaylandClient { @@ -114,9 +102,13 @@ public: * not only the one issued from event thread's waitForReading(), which means functions * called from dispatch_pending() can safely spin an event loop. */ + if (m_quitting) + return; + for (;;) { if (dispatchQueuePending() < 0) { - checkWaylandError(m_wldisplay); + Q_EMIT waylandError(); + m_quitting = true; return; } @@ -154,6 +146,7 @@ public: Q_SIGNALS: void needReadAndDispatch(); + void waylandError(); protected: void run() override @@ -328,11 +321,17 @@ QWaylandDisplay::QWaylandDisplay(QWaylandIntegration *waylandIntegration) qRegisterMetaType<uint32_t>("uint32_t"); mDisplay = wl_display_connect(nullptr); - if (!mDisplay) { + if (mDisplay) { + setupConnection(); + } else { qErrnoWarning(errno, "Failed to create wl_display"); - return; } + mWaylandTryReconnect = qEnvironmentVariableIsSet("QT_WAYLAND_RECONNECT"); +} + +void QWaylandDisplay::setupConnection() +{ struct ::wl_registry *registry = wl_display_get_registry(mDisplay); init(registry); @@ -404,7 +403,110 @@ void QWaylandDisplay::ensureScreen() Q_ASSERT(!QGuiApplication::screens().empty()); } -// Called in main thread, either from queued signal or directly. +void QWaylandDisplay::reconnect() +{ + qCWarning(lcQpaWayland) << "Attempting wayland reconnect"; + m_eventThread->stop(); + m_frameEventQueueThread->stop(); + m_eventThread->wait(); + m_frameEventQueueThread->wait(); + + qDeleteAll(mWaitingScreens); + mWaitingScreens.clear(); + + // mCompositor + mShm.reset(); + mCursorThemes.clear(); + mCursor.reset(); + mDndSelectionHandler.reset(); + mWindowExtension.reset(); + mSubCompositor.reset(); + mTouchExtension.reset(); + mQtKeyExtension.reset(); + mWindowManagerIntegration.reset(); + mTabletManager.reset(); + mPointerGestures.reset(); +#if QT_CONFIG(wayland_client_primary_selection) + mPrimarySelectionManager.reset(); +#endif + mTextInputMethodManager.reset(); + mTextInputManagerv1.reset(); + mTextInputManagerv2.reset(); + mTextInputManagerv4.reset(); + mHardwareIntegration.reset(); + mXdgOutputManager.reset(); + mViewporter.reset(); + mFractionalScaleManager.reset(); + + mWaylandIntegration->reset(); + + qDeleteAll(std::exchange(mInputDevices, {})); + mLastInputDevice = nullptr; + + auto screens = mScreens; + mScreens.clear(); + + for (const RegistryGlobal &global : mGlobals) { + emit globalRemoved(global); + } + mGlobals.clear(); + + mLastInputSerial = 0; + mLastInputWindow.clear(); + mLastKeyboardFocus.clear(); + mActiveWindows.clear(); + + const auto windows = QGuiApplication::allWindows(); + for (auto window : windows) { + if (auto waylandWindow = dynamic_cast<QWaylandWindow *>(window->handle())) + waylandWindow->closeChildPopups(); + } + // Remove windows that do not need to be recreated and now closed popups + QList<QWaylandWindow *> recreateWindows; + for (auto window : std::as_const(windows)) { + auto waylandWindow = dynamic_cast<QWaylandWindow*>((window)->handle()); + if (waylandWindow && waylandWindow->wlSurface()) { + waylandWindow->reset(); + recreateWindows.push_back(waylandWindow); + } + } + + if (mSyncCallback) { + wl_callback_destroy(mSyncCallback); + mSyncCallback = nullptr; + } + + mDisplay = wl_display_connect(nullptr); + if (!mDisplay) + _exit(1); + + setupConnection(); + initialize(); + + if (m_frameEventQueue) + wl_event_queue_destroy(m_frameEventQueue); + initEventThread(); + + emit reconnected(); + + auto needsRecreate = [](QPlatformWindow *window) { + return window && !static_cast<QWaylandWindow *>(window)->wlSurface(); + }; + auto window = recreateWindows.begin(); + while (!recreateWindows.isEmpty()) { + if (!needsRecreate((*window)->QPlatformWindow::parent()) && !needsRecreate((*window)->transientParent())) { + (*window)->reinit(); + window = recreateWindows.erase(window); + } else { + ++window; + } + if (window == recreateWindows.end()) + window = recreateWindows.begin(); + } + + mWaylandIntegration->reconfigureInputContext(); +} + void QWaylandDisplay::flushRequests() { m_eventThread->readAndDispatchEvents(); @@ -419,6 +521,8 @@ void QWaylandDisplay::initEventThread() new EventThread(mDisplay, /* default queue */ nullptr, EventThread::EmitToDispatch)); connect(m_eventThread.get(), &EventThread::needReadAndDispatch, this, &QWaylandDisplay::flushRequests, Qt::QueuedConnection); + connect(m_eventThread.get(), &EventThread::waylandError, this, + &QWaylandDisplay::checkWaylandError, Qt::QueuedConnection); m_eventThread->start(); // wl_display_disconnect() free this. @@ -428,10 +532,31 @@ void QWaylandDisplay::initEventThread() m_frameEventQueueThread->start(); } +void QWaylandDisplay::checkWaylandError() +{ + int ecode = wl_display_get_error(mDisplay); + if ((ecode == EPIPE || ecode == ECONNRESET)) { + qWarning("The Wayland connection broke. Did the Wayland compositor die?"); + if (mWaylandTryReconnect) { + reconnect(); + return; + } + } else { + qWarning("The Wayland connection experienced a fatal error: %s", strerror(ecode)); + } + _exit(-1); +} + void QWaylandDisplay::blockingReadEvents() { - if (wl_display_dispatch(mDisplay) < 0) - checkWaylandError(mDisplay); + if (wl_display_dispatch(mDisplay) < 0) { + int ecode = wl_display_get_error(mDisplay); + if ((ecode == EPIPE || ecode == ECONNRESET)) + qWarning("The Wayland connection broke during blocking read event. Did the Wayland compositor die?"); + else + qWarning("The Wayland connection experienced a fatal error during blocking read event: %s", strerror(ecode)); + _exit(-1); + } } void QWaylandDisplay::checkTextInputProtocol() diff --git a/src/client/qwaylanddisplay_p.h b/src/client/qwaylanddisplay_p.h index 640f33b8e..f5c1bcbbd 100644 --- a/src/client/qwaylanddisplay_p.h +++ b/src/client/qwaylanddisplay_p.h @@ -200,10 +200,14 @@ public slots: void flushRequests(); signals: + void reconnected(); void globalAdded(const RegistryGlobal &global); void globalRemoved(const RegistryGlobal &global); private: + void checkWaylandError(); + void reconnect(); + void setupConnection(); void handleWaylandSync(); void requestWaylandSync(); @@ -283,6 +287,7 @@ private: QList<QWaylandWindow *> mActiveWindows; struct wl_callback *mSyncCallback = nullptr; static const wl_callback_listener syncCallbackListener; + bool mWaylandTryReconnect = false; bool mClientSideInputContextRequested = [] () { const QString& requested = QPlatformInputContextFactory::requested(); diff --git a/src/client/qwaylandintegration.cpp b/src/client/qwaylandintegration.cpp index 13bacc9be..45a247f9b 100644 --- a/src/client/qwaylandintegration.cpp +++ b/src/client/qwaylandintegration.cpp @@ -503,6 +503,17 @@ QWaylandShellIntegration *QWaylandIntegration::createShellIntegration(const QStr } } +void QWaylandIntegration::reset() +{ + mServerBufferIntegration.reset(); + mServerBufferIntegrationInitialized = false; + + mInputDeviceIntegration.reset(); + + mClientBufferIntegration.reset(); + mClientBufferIntegrationInitialized = false; +} + } QT_END_NAMESPACE diff --git a/src/client/qwaylandintegration_p.h b/src/client/qwaylandintegration_p.h index 6221e6893..1ba7a6339 100644 --- a/src/client/qwaylandintegration_p.h +++ b/src/client/qwaylandintegration_p.h @@ -103,6 +103,7 @@ protected: QScopedPointer<QWaylandDisplay> mDisplay; protected: + void reset(); virtual QPlatformNativeInterface *createPlatformNativeInterface(); QScopedPointer<QWaylandClientBufferIntegration> mClientBufferIntegration; diff --git a/src/client/qwaylandshmbackingstore.cpp b/src/client/qwaylandshmbackingstore.cpp index b9bfc9c81..779ee45c7 100644 --- a/src/client/qwaylandshmbackingstore.cpp +++ b/src/client/qwaylandshmbackingstore.cpp @@ -136,7 +136,18 @@ QWaylandShmBackingStore::QWaylandShmBackingStore(QWindow *window, QWaylandDispla : QPlatformBackingStore(window) , mDisplay(display) { - + QObject::connect(mDisplay, &QWaylandDisplay::reconnected, window, [this]() { + auto copy = mBuffers; + // clear available buffers so we create new ones + // actual deletion is deferred till after resize call so we can copy + // contents from the back buffer + mBuffers.clear(); + mFrontBuffer = nullptr; + // resize always resets mBackBuffer + if (mRequestedSize.isValid() && waylandWindow()) + resize(mRequestedSize); + qDeleteAll(copy); + }); } QWaylandShmBackingStore::~QWaylandShmBackingStore() diff --git a/src/client/qwaylandwindow.cpp b/src/client/qwaylandwindow.cpp index a77cce5ae..90f4c609a 100644 --- a/src/client/qwaylandwindow.cpp +++ b/src/client/qwaylandwindow.cpp @@ -291,11 +291,21 @@ void QWaylandWindow::reset() mFrameCallbackElapsedTimer.invalidate(); mWaitingForFrameCallback = false; } + if (mFrameCallbackCheckIntervalTimerId != -1) { + killTimer(mFrameCallbackCheckIntervalTimerId); + mFrameCallbackCheckIntervalTimerId = -1; + } + mFrameCallbackTimedOut = false; mWaitingToApplyConfigure = false; + mCanResize = true; + mResizeDirty = false; + mOpaqueArea = QRegion(); mMask = QRegion(); + mQueuedBuffer = nullptr; + mQueuedBufferDamage = QRegion(); mDisplay->handleWindowDestroyed(this); } @@ -1623,6 +1633,16 @@ void QWaylandWindow::closeChildPopups() { popup->reset(); } } + +void QWaylandWindow::reinit() +{ + if (window()->isVisible()) { + initWindow(); + if (hasPendingUpdateRequest()) + deliverUpdateRequest(); + } +} + } QT_END_NAMESPACE diff --git a/src/client/qwaylandwindow_p.h b/src/client/qwaylandwindow_p.h index 22b42a9a0..d0640e7eb 100644 --- a/src/client/qwaylandwindow_p.h +++ b/src/client/qwaylandwindow_p.h @@ -229,6 +229,9 @@ public: void removeChildPopup(QWaylandWindow* child); void closeChildPopups(); + virtual void reinit(); + void reset(); + public slots: void applyConfigure(); @@ -325,8 +328,6 @@ private: void initializeWlSurface(); bool shouldCreateShellSurface() const; bool shouldCreateSubSurface() const; - void reset(); - static void closePopups(QWaylandWindow *parent); QPlatformScreen *calculateScreenFromSurfaceEvents() const; void setOpaqueArea(const QRegion &opaqueArea); bool isOpaque() const; diff --git a/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp b/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp index 8221d5d98..ca7d58f39 100644 --- a/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp +++ b/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp @@ -140,6 +140,12 @@ void QWaylandEglWindow::invalidateSurface() m_contentFBO = nullptr; } +void QWaylandEglWindow::reinit() +{ + QWaylandWindow::reinit(); + m_clientBufferIntegration = static_cast<QWaylandEglClientBufferIntegration *>(mDisplay->clientBufferIntegration()); +} + EGLSurface QWaylandEglWindow::eglSurface() const { return m_eglSurface; diff --git a/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h b/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h index 5b9aa9874..cfcdd5779 100644 --- a/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h +++ b/src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h @@ -50,6 +50,7 @@ public: void bindContentFBO(); void invalidateSurface() override; + void reinit() override; private: QWaylandEglClientBufferIntegration *m_clientBufferIntegration = nullptr; diff --git a/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp b/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp index 0b64db5c7..050b3eba3 100644 --- a/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp +++ b/src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp @@ -195,6 +195,10 @@ QWaylandGLContext::QWaylandGLContext(EGLDisplay eglDisplay, QWaylandDisplay *dis const QSurfaceFormat &fmt, QPlatformOpenGLContext *share) : QEGLPlatformContext(fmt, share, eglDisplay), m_display(display) { + m_reconnectionWatcher = QObject::connect(m_display, &QWaylandDisplay::reconnected, [this]() { + invalidateContext(); + }); + switch (format().renderableType()) { case QSurfaceFormat::OpenVG: m_api = EGL_OPENVG_API; @@ -260,6 +264,7 @@ void QWaylandGLContext::destroyTemporaryOffscreenSurface(EGLSurface eglSurface) QWaylandGLContext::~QWaylandGLContext() { + QObject::disconnect(m_reconnectionWatcher); delete m_blitter; m_blitter = nullptr; if (m_decorationsContext != EGL_NO_CONTEXT) @@ -280,6 +285,10 @@ void QWaylandGLContext::endFrame() bool QWaylandGLContext::makeCurrent(QPlatformSurface *surface) { + if (!isValid()) { + return false; + } + // in QWaylandGLContext() we called eglBindAPI with the correct value. However, // eglBindAPI's documentation says: // "eglBindAPI defines the current rendering API for EGL in the thread it is called from" diff --git a/src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h b/src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h index 00ef99b1a..b985c6675 100644 --- a/src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h +++ b/src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h @@ -62,6 +62,7 @@ private: wl_surface *m_wlSurface = nullptr; wl_egl_window *m_eglWindow = nullptr; QWaylandEglWindow *m_currentWindow = nullptr; + QMetaObject::Connection m_reconnectionWatcher; }; } diff --git a/tests/auto/client/CMakeLists.txt b/tests/auto/client/CMakeLists.txt index 44cf32714..5ae005eaa 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 000000000..21ce68fd5 --- /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 000000000..93007d4cc --- /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 000000000..f96de8c73 --- /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 000000000..285870e01 --- /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 dd7311e41..9b2c7deae 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 b7d1de78d..6fd14371c 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 71f3775a5..43d417ff8 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 6803a646c..8d26361a4 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); |