summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Edmundson <davidedmundson@kde.org>2022-01-28 16:24:32 +0000
committerDavid Edmundson <davidedmundson@kde.org>2023-03-07 13:26:50 +0000
commit26d8603b523af973682b7e602f1158ae62f21c9b (patch)
treecf5223fea624e961af7150fdde44bdf1e9cfa27a
parent83599e16789f945ef15954f2a2aabb37b245e596 (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>
-rw-r--r--src/client/qwaylanddisplay.cpp161
-rw-r--r--src/client/qwaylanddisplay_p.h5
-rw-r--r--src/client/qwaylandintegration.cpp11
-rw-r--r--src/client/qwaylandintegration_p.h1
-rw-r--r--src/client/qwaylandshmbackingstore.cpp13
-rw-r--r--src/client/qwaylandwindow.cpp20
-rw-r--r--src/client/qwaylandwindow_p.h5
-rw-r--r--src/hardwareintegration/client/wayland-egl/qwaylandeglwindow.cpp6
-rw-r--r--src/hardwareintegration/client/wayland-egl/qwaylandeglwindow_p.h1
-rw-r--r--src/hardwareintegration/client/wayland-egl/qwaylandglcontext.cpp9
-rw-r--r--src/hardwareintegration/client/wayland-egl/qwaylandglcontext_p.h1
-rw-r--r--tests/auto/client/CMakeLists.txt1
-rw-r--r--tests/auto/client/reconnect/CMakeLists.txt11
-rw-r--r--tests/auto/client/reconnect/tst_reconnect.cpp210
-rw-r--r--tests/auto/client/reconnect/wl-socket.c166
-rw-r--r--tests/auto/client/reconnect/wl-socket.h34
-rw-r--r--tests/auto/client/shared/corecompositor.cpp12
-rw-r--r--tests/auto/client/shared/corecompositor.h4
-rw-r--r--tests/auto/client/shared/mockcompositor.cpp4
-rw-r--r--tests/auto/client/shared/mockcompositor.h2
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);