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
|
||||
|
||||
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()
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -103,6 +103,7 @@ protected:
|
||||
QScopedPointer<QWaylandDisplay> mDisplay;
|
||||
|
||||
protected:
|
||||
void reset();
|
||||
virtual QPlatformNativeInterface *createPlatformNativeInterface();
|
||||
|
||||
QScopedPointer<QWaylandClientBufferIntegration> mClientBufferIntegration;
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
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 {
|
||||
|
||||
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()
|
||||
|
@ -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;
|
||||
|
@ -6,8 +6,8 @@
|
||||
|
||||
namespace MockCompositor {
|
||||
|
||||
DefaultCompositor::DefaultCompositor(CompositorType t)
|
||||
: CoreCompositor(t)
|
||||
DefaultCompositor::DefaultCompositor(CompositorType t, int socketFd)
|
||||
: CoreCompositor(t, socketFd)
|
||||
{
|
||||
{
|
||||
Lock l(this);
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user