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>
This commit is contained in:
parent
0042b8040f
commit
358655d8db
@ -65,18 +65,6 @@
|
|||||||
|
|
||||||
#include <tuple> // for std::tie
|
#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
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
namespace QtWaylandClient {
|
namespace QtWaylandClient {
|
||||||
@ -114,9 +102,13 @@ public:
|
|||||||
* not only the one issued from event thread's waitForReading(), which means functions
|
* not only the one issued from event thread's waitForReading(), which means functions
|
||||||
* called from dispatch_pending() can safely spin an event loop.
|
* called from dispatch_pending() can safely spin an event loop.
|
||||||
*/
|
*/
|
||||||
|
if (m_quitting)
|
||||||
|
return;
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (dispatchQueuePending() < 0) {
|
if (dispatchQueuePending() < 0) {
|
||||||
checkWaylandError(m_wldisplay);
|
Q_EMIT waylandError();
|
||||||
|
m_quitting = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,6 +146,7 @@ public:
|
|||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void needReadAndDispatch();
|
void needReadAndDispatch();
|
||||||
|
void waylandError();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void run() override
|
void run() override
|
||||||
@ -328,11 +321,17 @@ QWaylandDisplay::QWaylandDisplay(QWaylandIntegration *waylandIntegration)
|
|||||||
qRegisterMetaType<uint32_t>("uint32_t");
|
qRegisterMetaType<uint32_t>("uint32_t");
|
||||||
|
|
||||||
mDisplay = wl_display_connect(nullptr);
|
mDisplay = wl_display_connect(nullptr);
|
||||||
if (!mDisplay) {
|
if (mDisplay) {
|
||||||
|
setupConnection();
|
||||||
|
} else {
|
||||||
qErrnoWarning(errno, "Failed to create wl_display");
|
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);
|
struct ::wl_registry *registry = wl_display_get_registry(mDisplay);
|
||||||
init(registry);
|
init(registry);
|
||||||
|
|
||||||
@ -404,7 +403,110 @@ void QWaylandDisplay::ensureScreen()
|
|||||||
Q_ASSERT(!QGuiApplication::screens().empty());
|
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()
|
void QWaylandDisplay::flushRequests()
|
||||||
{
|
{
|
||||||
m_eventThread->readAndDispatchEvents();
|
m_eventThread->readAndDispatchEvents();
|
||||||
@ -419,6 +521,8 @@ void QWaylandDisplay::initEventThread()
|
|||||||
new EventThread(mDisplay, /* default queue */ nullptr, EventThread::EmitToDispatch));
|
new EventThread(mDisplay, /* default queue */ nullptr, EventThread::EmitToDispatch));
|
||||||
connect(m_eventThread.get(), &EventThread::needReadAndDispatch, this,
|
connect(m_eventThread.get(), &EventThread::needReadAndDispatch, this,
|
||||||
&QWaylandDisplay::flushRequests, Qt::QueuedConnection);
|
&QWaylandDisplay::flushRequests, Qt::QueuedConnection);
|
||||||
|
connect(m_eventThread.get(), &EventThread::waylandError, this,
|
||||||
|
&QWaylandDisplay::checkWaylandError, Qt::QueuedConnection);
|
||||||
m_eventThread->start();
|
m_eventThread->start();
|
||||||
|
|
||||||
// wl_display_disconnect() free this.
|
// wl_display_disconnect() free this.
|
||||||
@ -428,10 +532,31 @@ void QWaylandDisplay::initEventThread()
|
|||||||
m_frameEventQueueThread->start();
|
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()
|
void QWaylandDisplay::blockingReadEvents()
|
||||||
{
|
{
|
||||||
if (wl_display_dispatch(mDisplay) < 0)
|
if (wl_display_dispatch(mDisplay) < 0) {
|
||||||
checkWaylandError(mDisplay);
|
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()
|
void QWaylandDisplay::checkTextInputProtocol()
|
||||||
|
@ -200,10 +200,14 @@ public slots:
|
|||||||
void flushRequests();
|
void flushRequests();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
void reconnected();
|
||||||
void globalAdded(const RegistryGlobal &global);
|
void globalAdded(const RegistryGlobal &global);
|
||||||
void globalRemoved(const RegistryGlobal &global);
|
void globalRemoved(const RegistryGlobal &global);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void checkWaylandError();
|
||||||
|
void reconnect();
|
||||||
|
void setupConnection();
|
||||||
void handleWaylandSync();
|
void handleWaylandSync();
|
||||||
void requestWaylandSync();
|
void requestWaylandSync();
|
||||||
|
|
||||||
@ -283,6 +287,7 @@ private:
|
|||||||
QList<QWaylandWindow *> mActiveWindows;
|
QList<QWaylandWindow *> mActiveWindows;
|
||||||
struct wl_callback *mSyncCallback = nullptr;
|
struct wl_callback *mSyncCallback = nullptr;
|
||||||
static const wl_callback_listener syncCallbackListener;
|
static const wl_callback_listener syncCallbackListener;
|
||||||
|
bool mWaylandTryReconnect = false;
|
||||||
|
|
||||||
bool mClientSideInputContextRequested = [] () {
|
bool mClientSideInputContextRequested = [] () {
|
||||||
const QString& requested = QPlatformInputContextFactory::requested();
|
const QString& requested = QPlatformInputContextFactory::requested();
|
||||||
|
@ -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
|
QT_END_NAMESPACE
|
||||||
|
@ -103,6 +103,7 @@ protected:
|
|||||||
QScopedPointer<QWaylandDisplay> mDisplay;
|
QScopedPointer<QWaylandDisplay> mDisplay;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void reset();
|
||||||
virtual QPlatformNativeInterface *createPlatformNativeInterface();
|
virtual QPlatformNativeInterface *createPlatformNativeInterface();
|
||||||
|
|
||||||
QScopedPointer<QWaylandClientBufferIntegration> mClientBufferIntegration;
|
QScopedPointer<QWaylandClientBufferIntegration> mClientBufferIntegration;
|
||||||
|
@ -136,7 +136,18 @@ QWaylandShmBackingStore::QWaylandShmBackingStore(QWindow *window, QWaylandDispla
|
|||||||
: QPlatformBackingStore(window)
|
: QPlatformBackingStore(window)
|
||||||
, mDisplay(display)
|
, 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()
|
QWaylandShmBackingStore::~QWaylandShmBackingStore()
|
||||||
|
@ -291,11 +291,21 @@ void QWaylandWindow::reset()
|
|||||||
mFrameCallbackElapsedTimer.invalidate();
|
mFrameCallbackElapsedTimer.invalidate();
|
||||||
mWaitingForFrameCallback = false;
|
mWaitingForFrameCallback = false;
|
||||||
}
|
}
|
||||||
|
if (mFrameCallbackCheckIntervalTimerId != -1) {
|
||||||
|
killTimer(mFrameCallbackCheckIntervalTimerId);
|
||||||
|
mFrameCallbackCheckIntervalTimerId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
mFrameCallbackTimedOut = false;
|
mFrameCallbackTimedOut = false;
|
||||||
mWaitingToApplyConfigure = false;
|
mWaitingToApplyConfigure = false;
|
||||||
|
mCanResize = true;
|
||||||
|
mResizeDirty = false;
|
||||||
|
|
||||||
|
mOpaqueArea = QRegion();
|
||||||
mMask = QRegion();
|
mMask = QRegion();
|
||||||
|
|
||||||
mQueuedBuffer = nullptr;
|
mQueuedBuffer = nullptr;
|
||||||
|
mQueuedBufferDamage = QRegion();
|
||||||
|
|
||||||
mDisplay->handleWindowDestroyed(this);
|
mDisplay->handleWindowDestroyed(this);
|
||||||
}
|
}
|
||||||
@ -1623,6 +1633,16 @@ void QWaylandWindow::closeChildPopups() {
|
|||||||
popup->reset();
|
popup->reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QWaylandWindow::reinit()
|
||||||
|
{
|
||||||
|
if (window()->isVisible()) {
|
||||||
|
initWindow();
|
||||||
|
if (hasPendingUpdateRequest())
|
||||||
|
deliverUpdateRequest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
@ -229,6 +229,9 @@ public:
|
|||||||
void removeChildPopup(QWaylandWindow* child);
|
void removeChildPopup(QWaylandWindow* child);
|
||||||
void closeChildPopups();
|
void closeChildPopups();
|
||||||
|
|
||||||
|
virtual void reinit();
|
||||||
|
void reset();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void applyConfigure();
|
void applyConfigure();
|
||||||
|
|
||||||
@ -325,8 +328,6 @@ private:
|
|||||||
void initializeWlSurface();
|
void initializeWlSurface();
|
||||||
bool shouldCreateShellSurface() const;
|
bool shouldCreateShellSurface() const;
|
||||||
bool shouldCreateSubSurface() const;
|
bool shouldCreateSubSurface() const;
|
||||||
void reset();
|
|
||||||
static void closePopups(QWaylandWindow *parent);
|
|
||||||
QPlatformScreen *calculateScreenFromSurfaceEvents() const;
|
QPlatformScreen *calculateScreenFromSurfaceEvents() const;
|
||||||
void setOpaqueArea(const QRegion &opaqueArea);
|
void setOpaqueArea(const QRegion &opaqueArea);
|
||||||
bool isOpaque() const;
|
bool isOpaque() const;
|
||||||
|
@ -16,6 +16,7 @@ if (NOT WEBOS)
|
|||||||
add_subdirectory(nooutput)
|
add_subdirectory(nooutput)
|
||||||
add_subdirectory(output)
|
add_subdirectory(output)
|
||||||
add_subdirectory(primaryselectionv1)
|
add_subdirectory(primaryselectionv1)
|
||||||
|
add_subdirectory(reconnect)
|
||||||
add_subdirectory(seatv4)
|
add_subdirectory(seatv4)
|
||||||
add_subdirectory(seatv7)
|
add_subdirectory(seatv7)
|
||||||
add_subdirectory(seat)
|
add_subdirectory(seat)
|
||||||
|
11
tests/auto/wayland/reconnect/CMakeLists.txt
Normal file
11
tests/auto/wayland/reconnect/CMakeLists.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#####################################################################
|
||||||
|
## tst_client Test:
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
qt_internal_add_test(tst_reconnect
|
||||||
|
SOURCES
|
||||||
|
wl-socket.c
|
||||||
|
tst_reconnect.cpp
|
||||||
|
PUBLIC_LIBRARIES
|
||||||
|
SharedClientTest
|
||||||
|
)
|
210
tests/auto/wayland/reconnect/tst_reconnect.cpp
Normal file
210
tests/auto/wayland/reconnect/tst_reconnect.cpp
Normal file
@ -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"
|
166
tests/auto/wayland/reconnect/wl-socket.c
Normal file
166
tests/auto/wayland/reconnect/wl-socket.c
Normal file
@ -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;
|
||||||
|
}
|
34
tests/auto/wayland/reconnect/wl-socket.h
Normal file
34
tests/auto/wayland/reconnect/wl-socket.h
Normal file
@ -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
|
@ -6,10 +6,9 @@
|
|||||||
|
|
||||||
namespace MockCompositor {
|
namespace MockCompositor {
|
||||||
|
|
||||||
CoreCompositor::CoreCompositor(CompositorType t)
|
CoreCompositor::CoreCompositor(CompositorType t, int socketFd)
|
||||||
: m_type(t)
|
: m_type(t)
|
||||||
, m_display(wl_display_create())
|
, m_display(wl_display_create())
|
||||||
, m_socketName(wl_display_add_socket_auto(m_display))
|
|
||||||
, m_eventLoop(wl_display_get_event_loop(m_display))
|
, m_eventLoop(wl_display_get_event_loop(m_display))
|
||||||
|
|
||||||
// Start dispatching
|
// 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();
|
m_timer.start();
|
||||||
Q_ASSERT(isClean());
|
Q_ASSERT(isClean());
|
||||||
}
|
}
|
||||||
@ -29,7 +33,9 @@ CoreCompositor::~CoreCompositor()
|
|||||||
{
|
{
|
||||||
m_running = false;
|
m_running = false;
|
||||||
m_dispatchThread.join();
|
m_dispatchThread.join();
|
||||||
|
wl_display_destroy_clients(m_display);
|
||||||
wl_display_destroy(m_display);
|
wl_display_destroy(m_display);
|
||||||
|
qDebug() << "cleanup";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CoreCompositor::isClean()
|
bool CoreCompositor::isClean()
|
||||||
|
@ -29,7 +29,8 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
CompositorType m_type = Default;
|
CompositorType m_type = Default;
|
||||||
explicit CoreCompositor(CompositorType t = Default);
|
explicit CoreCompositor(CompositorType t = Default, int socketFd = -1);
|
||||||
|
|
||||||
~CoreCompositor();
|
~CoreCompositor();
|
||||||
bool isClean();
|
bool isClean();
|
||||||
QString dirtyMessage();
|
QString dirtyMessage();
|
||||||
@ -178,7 +179,6 @@ protected:
|
|||||||
CoreCompositor *m_compositor = nullptr;
|
CoreCompositor *m_compositor = nullptr;
|
||||||
std::thread::id m_threadId;
|
std::thread::id m_threadId;
|
||||||
};
|
};
|
||||||
QByteArray m_socketName;
|
|
||||||
wl_event_loop *m_eventLoop = nullptr;
|
wl_event_loop *m_eventLoop = nullptr;
|
||||||
bool m_running = true;
|
bool m_running = true;
|
||||||
QList<Global *> m_globals;
|
QList<Global *> m_globals;
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
namespace MockCompositor {
|
namespace MockCompositor {
|
||||||
|
|
||||||
DefaultCompositor::DefaultCompositor(CompositorType t)
|
DefaultCompositor::DefaultCompositor(CompositorType t, int socketFd)
|
||||||
: CoreCompositor(t)
|
: CoreCompositor(t, socketFd)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
Lock l(this);
|
Lock l(this);
|
||||||
|
@ -32,7 +32,7 @@ namespace MockCompositor {
|
|||||||
class DefaultCompositor : public CoreCompositor
|
class DefaultCompositor : public CoreCompositor
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit DefaultCompositor(CompositorType t = CompositorType::Default);
|
explicit DefaultCompositor(CompositorType t = CompositorType::Default, int socketFd = -1);
|
||||||
// Convenience functions
|
// Convenience functions
|
||||||
Output *output(int i = 0) { return getAll<Output>().value(i, nullptr); }
|
Output *output(int i = 0) { return getAll<Output>().value(i, nullptr); }
|
||||||
Surface *surface(int i = 0);
|
Surface *surface(int i = 0);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user