diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index edc9dcea4f..b751f53ffc 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,4 +1,5 @@ add_definitions(-DKWIN_UNIT_TEST) +add_subdirectory(wayland) ######################################################## # Test ScreenPaintData diff --git a/autotests/wayland/CMakeLists.txt b/autotests/wayland/CMakeLists.txt new file mode 100644 index 0000000000..43b8eb8bc0 --- /dev/null +++ b/autotests/wayland/CMakeLists.txt @@ -0,0 +1,10 @@ +add_definitions(-DKWINBACKENDPATH="${CMAKE_BINARY_DIR}/backends/virtual/KWinWaylandVirtualBackend.so") +add_definitions(-DKWINQPAPATH="${CMAKE_BINARY_DIR}/plugins/qpa/") +######################################################## +# Test Start +######################################################## +set( testStart_SRCS start_test.cpp kwin_wayland_test.cpp ) +add_executable(testStart ${testStart_SRCS}) +target_link_libraries( testStart kwin Qt5::Test) +add_test(kwin-testStart testStart) +ecm_mark_as_test(testStart) diff --git a/autotests/wayland/kwin_wayland_test.cpp b/autotests/wayland/kwin_wayland_test.cpp new file mode 100644 index 0000000000..e70301609f --- /dev/null +++ b/autotests/wayland/kwin_wayland_test.cpp @@ -0,0 +1,260 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "../../abstract_backend.h" +#include "../../wayland_server.h" +#include "../../workspace.h" +#include "../../xcbutils.h" + +#include +#include +#include +#include +#include + +// system +#include +#include +#include + +namespace KWin +{ + +static void readDisplay(int pipe); + +WaylandTestApplication::WaylandTestApplication(int &argc, char **argv) + : Application(OperationModeXwayland, argc, argv) +{ + WaylandServer *server = WaylandServer::create(this); + QPluginLoader loader(QStringLiteral(KWINBACKENDPATH)); + loader.instance()->setParent(server); +} + +WaylandTestApplication::~WaylandTestApplication() +{ + destroyCompositor(); + destroyWorkspace(); + if (x11Connection()) { + Xcb::setInputFocus(XCB_INPUT_FOCUS_POINTER_ROOT); + destroyAtoms(); + xcb_disconnect(x11Connection()); + } + if (m_xwaylandProcess) { + m_xwaylandProcess->terminate(); + m_xwaylandProcess->waitForFinished(); + } +} + +void WaylandTestApplication::performStartup() +{ + // first load options - done internally by a different thread + createOptions(); + waylandServer()->createInternalConnection(); + + // try creating the Wayland Backend + createInput(); + createBackend(); +} + +void WaylandTestApplication::createBackend() +{ + AbstractBackend *backend = waylandServer()->backend(); + connect(backend, &AbstractBackend::screensQueried, this, &WaylandTestApplication::continueStartupWithScreens); + connect(backend, &AbstractBackend::initFailed, this, + [] () { + std::cerr << "FATAL ERROR: backend failed to initialize, exiting now" << std::endl; + ::exit(1); + } + ); + backend->init(); +} + +void WaylandTestApplication::continueStartupWithScreens() +{ + disconnect(waylandServer()->backend(), &AbstractBackend::screensQueried, this, &WaylandTestApplication::continueStartupWithScreens); + createScreens(); + waylandServer()->initOutputs(); + + createCompositor(); + + startXwaylandServer(); +} + +void WaylandTestApplication::continueStartupWithX() +{ + createX11Connection(); + xcb_connection_t *c = x11Connection(); + if (!c) { + // about to quit + return; + } + QSocketNotifier *notifier = new QSocketNotifier(xcb_get_file_descriptor(c), QSocketNotifier::Read, this); + auto processXcbEvents = [this, c] { + while (auto event = xcb_poll_for_event(c)) { + updateX11Time(event); + long result = 0; + if (QThread::currentThread()->eventDispatcher()->filterNativeEvent(QByteArrayLiteral("xcb_generic_event_t"), event, &result)) { + free(event); + continue; + } + if (Workspace::self()) { + Workspace::self()->workspaceEvent(event); + } + free(event); + } + xcb_flush(c); + }; + connect(notifier, &QSocketNotifier::activated, this, processXcbEvents); + connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, processXcbEvents); + connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::awake, this, processXcbEvents); + + // create selection owner for WM_S0 - magic X display number expected by XWayland + KSelectionOwner owner("WM_S0", c, x11RootWindow()); + owner.claim(true); + + createAtoms(); + + setupEventFilters(); + + // Check whether another windowmanager is running + const uint32_t maskValues[] = {XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT}; + ScopedCPointer redirectCheck(xcb_request_check(connection(), + xcb_change_window_attributes_checked(connection(), + rootWindow(), + XCB_CW_EVENT_MASK, + maskValues))); + if (!redirectCheck.isNull()) { + ::exit(1); + } + + createWorkspace(); + + Xcb::sync(); // Trigger possible errors, there's still a chance to abort +} + +void WaylandTestApplication::createX11Connection() +{ + int screenNumber = 0; + xcb_connection_t *c = nullptr; + if (m_xcbConnectionFd == -1) { + c = xcb_connect(nullptr, &screenNumber); + } else { + c = xcb_connect_to_fd(m_xcbConnectionFd, nullptr); + } + if (int error = xcb_connection_has_error(c)) { + std::cerr << "FATAL ERROR: Creating connection to XServer failed: " << error << std::endl; + exit(1); + return; + } + setX11Connection(c); + // we don't support X11 multi-head in Wayland + setX11ScreenNumber(screenNumber); + setX11RootWindow(defaultScreen()->root); +} + +void WaylandTestApplication::startXwaylandServer() +{ + int pipeFds[2]; + if (pipe(pipeFds) != 0) { + std::cerr << "FATAL ERROR failed to create pipe to start Xwayland " << std::endl; + exit(1); + return; + } + int sx[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) { + std::cerr << "FATAL ERROR: failed to open socket to open XCB connection" << std::endl; + exit(1); + return; + } + int fd = dup(sx[1]); + if (fd < 0) { + std::cerr << "FATAL ERROR: failed to open socket to open XCB connection" << std::endl; + exit(20); + return; + } + + const int waylandSocket = waylandServer()->createXWaylandConnection(); + if (waylandSocket == -1) { + std::cerr << "FATAL ERROR: failed to open socket for Xwayland" << std::endl; + exit(1); + return; + } + const int wlfd = dup(waylandSocket); + if (wlfd < 0) { + std::cerr << "FATAL ERROR: failed to open socket for Xwayland" << std::endl; + exit(20); + return; + } + + m_xcbConnectionFd = sx[0]; + + m_xwaylandProcess = new QProcess(kwinApp()); + m_xwaylandProcess->setProgram(QStringLiteral("Xwayland")); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("WAYLAND_SOCKET", QByteArray::number(wlfd)); + m_xwaylandProcess->setProcessEnvironment(env); + m_xwaylandProcess->setArguments({QStringLiteral("-displayfd"), + QString::number(pipeFds[1]), + QStringLiteral("-rootless"), + QStringLiteral("-wm"), + QString::number(fd)}); + connect(m_xwaylandProcess, static_cast(&QProcess::error), this, + [] (QProcess::ProcessError error) { + if (error == QProcess::FailedToStart) { + std::cerr << "FATAL ERROR: failed to start Xwayland" << std::endl; + } else { + std::cerr << "FATAL ERROR: Xwayland failed, going to exit now" << std::endl; + } + exit(1); + } + ); + const int xDisplayPipe = pipeFds[0]; + connect(m_xwaylandProcess, &QProcess::started, this, + [this, xDisplayPipe] { + QFutureWatcher *watcher = new QFutureWatcher(this); + QObject::connect(watcher, &QFutureWatcher::finished, this, &WaylandTestApplication::continueStartupWithX, Qt::QueuedConnection); + QObject::connect(watcher, &QFutureWatcher::finished, watcher, &QFutureWatcher::deleteLater, Qt::QueuedConnection); + watcher->setFuture(QtConcurrent::run(readDisplay, xDisplayPipe)); + } + ); + m_xwaylandProcess->start(); + close(pipeFds[1]); +} + +static void readDisplay(int pipe) +{ + QFile readPipe; + if (!readPipe.open(pipe, QIODevice::ReadOnly)) { + std::cerr << "FATAL ERROR failed to open pipe to start X Server" << std::endl; + exit(1); + } + QByteArray displayNumber = readPipe.readLine(); + + displayNumber.prepend(QByteArray(":")); + displayNumber.remove(displayNumber.size() -1, 1); + std::cout << "X-Server started on display " << displayNumber.constData() << std::endl; + + setenv("DISPLAY", displayNumber.constData(), true); + + // close our pipe + close(pipe); +} + +} diff --git a/autotests/wayland/kwin_wayland_test.h b/autotests/wayland/kwin_wayland_test.h new file mode 100644 index 0000000000..772a23be3e --- /dev/null +++ b/autotests/wayland/kwin_wayland_test.h @@ -0,0 +1,67 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#ifndef KWIN_WAYLAND_TEST_H +#define KWIN_WAYLAND_TEST_H + +#include "../../main.h" + +// Qt +#include + +namespace KWin +{ + +class WaylandTestApplication : public Application +{ + Q_OBJECT +public: + WaylandTestApplication(int &argc, char **argv); + virtual ~WaylandTestApplication(); + +protected: + void performStartup() override; + +private: + void createBackend(); + void createX11Connection(); + void continueStartupWithScreens(); + void continueStartupWithX(); + void startXwaylandServer(); + + int m_xcbConnectionFd = -1; + QProcess *m_xwaylandProcess = nullptr; +}; + +} + +#define WAYLANTEST_MAIN(TestObject) \ +int main(int argc, char *argv[]) \ +{ \ + setenv("QT_QPA_PLATFORM", "wayland-org.kde.kwin.qpa", true); \ + setenv("QT_QPA_PLATFORM_PLUGIN_PATH", KWINQPAPATH, true); \ + setenv("KWIN_FOCRE_OWN_QPA", "1", true); \ + KWin::WaylandTestApplication app(argc, argv); \ + app.setAttribute(Qt::AA_Use96Dpi, true); \ + TestObject tc; \ + QTEST_SET_MAIN_SOURCE_PATH \ + return QTest::qExec(&tc, argc, argv); \ +} + +#endif diff --git a/autotests/wayland/start_test.cpp b/autotests/wayland/start_test.cpp new file mode 100644 index 0000000000..81a1145337 --- /dev/null +++ b/autotests/wayland/start_test.cpp @@ -0,0 +1,164 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "abstract_backend.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_start_test-0"); + +class StartTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testScreens(); + void testNoWindowsAtStart(); + void testCreateWindow(); +}; + +void StartTest::initTestCase() +{ + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + waylandServer()->backend()->setInitialWindowSize(QSize(1280, 1024)); + waylandServer()->init(s_socketName.toLocal8Bit()); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); +} + +void StartTest::testScreens() +{ + QCOMPARE(screens()->count(), 1); + QCOMPARE(screens()->size(), QSize(1280, 1024)); + QCOMPARE(screens()->geometry(), QRect(0, 0, 1280, 1024)); +} + +void StartTest::testNoWindowsAtStart() +{ + QVERIFY(workspace()->clientList().isEmpty()); + QVERIFY(workspace()->desktopList().isEmpty()); + QVERIFY(workspace()->allClientList().isEmpty()); + QVERIFY(workspace()->deletedList().isEmpty()); + QVERIFY(workspace()->unmanagedList().isEmpty()); + QVERIFY(waylandServer()->clients().isEmpty()); +} + +void StartTest::testCreateWindow() +{ + // first we need to connect to the server + using namespace KWayland::Client; + auto connection = new ConnectionThread; + QSignalSpy connectedSpy(connection, &ConnectionThread::connected); + QVERIFY(connectedSpy.isValid()); + connection->setSocketName(s_socketName); + QThread *thread = new QThread(this); + connection->moveToThread(thread); + thread->start(); + connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + QSignalSpy shellClientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(shellClientAddedSpy.isValid()); + QSignalSpy shellClientRemovedSpy(waylandServer(), &WaylandServer::shellClientRemoved); + QVERIFY(shellClientRemovedSpy.isValid()); + + { + EventQueue queue; + queue.setup(connection); + Registry registry; + registry.setEventQueue(&queue); + registry.create(connection); + QSignalSpy registryAnnouncedSpy(®istry, &Registry::interfacesAnnounced); + QVERIFY(registryAnnouncedSpy.isValid()); + registry.setup(); + QVERIFY(registryAnnouncedSpy.wait()); + + const auto compositorData = registry.interface(Registry::Interface::Compositor); + const auto shellData = registry.interface(Registry::Interface::Shell); + const auto shmData = registry.interface(Registry::Interface::Shm); + QScopedPointer compositor(registry.createCompositor(compositorData.name, compositorData.version)); + QVERIFY(!compositor.isNull()); + QScopedPointer shell(registry.createShell(shellData.name, shellData.version)); + QVERIFY(!shell.isNull()); + + QScopedPointer surface(compositor->createSurface()); + QVERIFY(!surface.isNull()); + QSignalSpy surfaceRenderedSpy(surface.data(), &Surface::frameRendered); + QVERIFY(surfaceRenderedSpy.isValid()); + + QScopedPointer shellSurface(shell->createSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + connection->flush(); + QVERIFY(waylandServer()->clients().isEmpty()); + // now dispatch should give us the client + waylandServer()->dispatch(); + QCOMPARE(waylandServer()->clients().count(), 1); + // but still not yet in workspace + QVERIFY(workspace()->allClientList().isEmpty()); + + // let's render + QImage img(QSize(100, 50), QImage::Format_ARGB32); + img.fill(Qt::blue); + QScopedPointer shm(registry.createShmPool(shmData.name, shmData.version)); + QVERIFY(!shm.isNull()); + surface->attachBuffer(shm->createBuffer(img)); + surface->damage(QRect(0, 0, 100, 50)); + surface->commit(); + + connection->flush(); + QVERIFY(shellClientAddedSpy.wait()); + QCOMPARE(workspace()->allClientList().count(), 1); + QCOMPARE(workspace()->allClientList().first(), waylandServer()->clients().first()); + QVERIFY(workspace()->activeClient()); + QCOMPARE(workspace()->activeClient()->pos(), QPoint(0, 0)); + QCOMPARE(workspace()->activeClient()->size(), QSize(100, 50)); + QCOMPARE(workspace()->activeClient()->geometry(), QRect(0, 0, 100, 50)); + + // and kwin will render it + QVERIFY(surfaceRenderedSpy.wait()); + } + // this should tear down everything again + QVERIFY(shellClientRemovedSpy.wait()); + QVERIFY(waylandServer()->clients().isEmpty()); + + // cleanup + connection->deleteLater(); + thread->quit(); + thread->wait(); +} + +} + +WAYLANTEST_MAIN(KWin::StartTest) +#include "start_test.moc" diff --git a/plugins/qpa/main.cpp b/plugins/qpa/main.cpp index 36df4a8c86..07b3ed09d4 100644 --- a/plugins/qpa/main.cpp +++ b/plugins/qpa/main.cpp @@ -33,7 +33,7 @@ public: QPlatformIntegration *KWinIntegrationPlugin::create(const QString &system, const QStringList ¶mList) { Q_UNUSED(paramList) - if (!QCoreApplication::applicationFilePath().endsWith(QLatin1Literal("kwin_wayland"))) { + if (!QCoreApplication::applicationFilePath().endsWith(QLatin1Literal("kwin_wayland")) && !qEnvironmentVariableIsSet("KWIN_FOCRE_OWN_QPA")) { // Not KWin return nullptr; }