diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4079017577..e6e3cd13ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -302,6 +302,7 @@ add_library(KWinXwaylandServerModule OBJECT xwl/selection_source.cpp xwl/transfer.cpp xwl/xwayland.cpp + xwl/xwaylandsocket.cpp ${xwaylandlogging_SOURCES} ) diff --git a/src/xwl/xwayland.cpp b/src/xwl/xwayland.cpp index 92e3d9199d..b4f7327dd9 100644 --- a/src/xwl/xwayland.cpp +++ b/src/xwl/xwayland.cpp @@ -10,6 +10,7 @@ */ #include "xwayland.h" #include "databridge.h" +#include "xwaylandsocket.h" #include "main_wayland.h" #include "options.h" @@ -27,6 +28,7 @@ #include #include #include +#include #include #include @@ -42,22 +44,6 @@ #include #include -static int readDisplay(int pipe) -{ - int display = -1; - QFile readPipe; - - if (!readPipe.open(pipe, QIODevice::ReadOnly)) { - qCWarning(KWIN_XWL) << "Failed to open X11 display name pipe:" << readPipe.errorString(); - } else { - display = readPipe.readLine().trimmed().toInt(); - } - - // close our pipe - close(pipe); - return display; -} - namespace KWin { namespace Xwl @@ -88,6 +74,35 @@ void Xwayland::start() return; } + QScopedPointer socket(new XwaylandSocket()); + if (!socket->isValid()) { + qCWarning(KWIN_XWL) << "Failed to create Xwayland connection sockets"; + emit errorOccurred(); + return; + } + + // The abstract socket file descriptor will be passed to Xwayland and closed by us. + const int abstractSocket = dup(socket->abstractFileDescriptor()); + if (abstractSocket == -1) { + qCWarning(KWIN_XWL, "Failed to duplicate file descriptor: %s", strerror(errno)); + emit errorOccurred(); + return; + } + auto abstractSocketCleanup = qScopeGuard([&abstractSocket]() { + close(abstractSocket); + }); + + // The unix socket file descriptor will be passed to Xwayland and closed by us. + const int unixSocket = dup(socket->unixFileDescriptor()); + if (unixSocket == -1) { + qCWarning(KWIN_XWL, "Failed to duplicate file descriptor: %s", strerror(errno)); + emit errorOccurred(); + return; + } + auto unixSocketCleanup = qScopeGuard([&unixSocket]() { + close(unixSocket); + }); + int pipeFds[2]; if (pipe(pipeFds) != 0) { qCWarning(KWIN_XWL, "Failed to create pipe to start Xwayland: %s", strerror(errno)); @@ -127,7 +142,7 @@ void Xwayland::start() } m_xcbConnectionFd = sx[0]; - m_displayFileDescriptor = pipeFds[0]; + m_socket.reset(socket.take()); m_xwaylandProcess = new Process(this); m_xwaylandProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); @@ -139,15 +154,22 @@ void Xwayland::start() env.insert("WAYLAND_DEBUG", QByteArrayLiteral("1")); } m_xwaylandProcess->setProcessEnvironment(env); - m_xwaylandProcess->setArguments({QStringLiteral("-displayfd"), - QString::number(pipeFds[1]), + m_xwaylandProcess->setArguments({m_socket->name(), + QStringLiteral("-displayfd"), QString::number(pipeFds[1]), QStringLiteral("-rootless"), QStringLiteral("-wm"), QString::number(fd), - QStringLiteral("-auth"), m_authorityFile.fileName()}); + QStringLiteral("-auth"), m_authorityFile.fileName(), + QStringLiteral("-listen"), QString::number(abstractSocket), + QStringLiteral("-listen"), QString::number(unixSocket)}); connect(m_xwaylandProcess, &QProcess::errorOccurred, this, &Xwayland::handleXwaylandError); - connect(m_xwaylandProcess, &QProcess::started, this, &Xwayland::handleXwaylandStarted); connect(m_xwaylandProcess, QOverload::of(&QProcess::finished), this, &Xwayland::handleXwaylandFinished); + + // When Xwayland starts writing the display name to displayfd, it is ready. Alternatively, + // the Xwayland can send us the SIGUSR1 signal, but it's already reserved for VT hand-off. + m_readyNotifier = new QSocketNotifier(pipeFds[0], QSocketNotifier::Read, this); + connect(m_readyNotifier, &QSocketNotifier::activated, this, &Xwayland::handleXwaylandReady); + m_xwaylandProcess->start(); close(pipeFds[1]); } @@ -163,6 +185,7 @@ void Xwayland::stop() // If Xwayland has crashed, we must deactivate the socket notifier and ensure that no X11 // events will be dispatched before blocking; otherwise we will simply hang... uninstallSocketNotifier(); + maybeDestroyReadyNotifier(); DataBridge::destroy(); m_selectionOwner.reset(); @@ -180,6 +203,7 @@ void Xwayland::stop() delete m_xwaylandProcess; m_xwaylandProcess = nullptr; + m_socket.reset(); waylandServer()->destroyXWaylandConnection(); // This one must be destroyed last! m_app->setClosingX11Connection(false); @@ -238,13 +262,6 @@ void Xwayland::uninstallSocketNotifier() m_socketNotifier = nullptr; } -void Xwayland::handleXwaylandStarted() -{ - m_watcher = new QFutureWatcher(this); - connect(m_watcher, &QFutureWatcher::finished, this, &Xwayland::handleXwaylandReady); - m_watcher->setFuture(QtConcurrent::run(readDisplay, m_displayFileDescriptor)); -} - void Xwayland::handleXwaylandFinished(int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(KWIN_XWL) << "Xwayland process has quit with exit code" << exitCode; @@ -312,17 +329,16 @@ void Xwayland::handleXwaylandError(QProcess::ProcessError error) void Xwayland::handleXwaylandReady() { - m_display = m_watcher->result(); - - m_watcher->deleteLater(); - m_watcher = nullptr; + // We don't care what Xwayland writes to the displayfd, we just want to know when it's ready. + maybeDestroyReadyNotifier(); if (!createX11Connection() || !writeXauthorityEntries()) { emit errorOccurred(); return; } - const QByteArray displayName = ':' + QByteArray::number(m_display); + const QByteArray displayName = ':' + QByteArray::number(m_socket->display()); + qCInfo(KWIN_XWL) << "Xwayland server started on display" << displayName; qputenv("DISPLAY", displayName); qputenv("XAUTHORITY", m_authorityFile.fileName().toUtf8()); @@ -343,6 +359,16 @@ void Xwayland::handleXwaylandReady() Xcb::sync(); // Trigger possible errors, there's still a chance to abort } +void Xwayland::maybeDestroyReadyNotifier() +{ + if (m_readyNotifier) { + close(m_readyNotifier->socket()); + + delete m_readyNotifier; + m_readyNotifier = nullptr; + } +} + bool Xwayland::createX11Connection() { xcb_connection_t *connection = xcb_connect_to_fd(m_xcbConnectionFd, nullptr); @@ -436,7 +462,7 @@ static QByteArray generateXauthorityCookie() bool Xwayland::writeXauthorityEntries() { const QByteArray hostname = QHostInfo::localHostName().toUtf8(); - const QByteArray display = QByteArray::number(m_display); + const QByteArray display = QByteArray::number(m_socket->display()); const QByteArray name = QByteArrayLiteral("MIT-MAGIC-COOKIE-1"); const QByteArray cookie = generateXauthorityCookie(); diff --git a/src/xwl/xwayland.h b/src/xwl/xwayland.h index 0bdbdbfdf5..e18e3e2330 100644 --- a/src/xwl/xwayland.h +++ b/src/xwl/xwayland.h @@ -12,7 +12,6 @@ #include "xwayland_interface.h" -#include #include #include #include @@ -22,6 +21,7 @@ class KSelectionOwner; namespace KWin { class ApplicationWaylandAbstract; +class XwaylandSocket; namespace Xwl { @@ -85,7 +85,6 @@ private Q_SLOTS: void dispatchEvents(); void resetCrashCount(); - void handleXwaylandStarted(); void handleXwaylandFinished(int exitCode, QProcess::ExitStatus exitStatus); void handleXwaylandCrashed(); void handleXwaylandError(QProcess::ProcessError error); @@ -94,6 +93,7 @@ private Q_SLOTS: private: void installSocketNotifier(); void uninstallSocketNotifier(); + void maybeDestroyReadyNotifier(); bool createX11Connection(); void destroyX11Connection(); @@ -103,16 +103,15 @@ private: DragEventReply dragMoveFilter(Toplevel *target, const QPoint &pos) override; - int m_displayFileDescriptor = -1; int m_xcbConnectionFd = -1; QProcess *m_xwaylandProcess = nullptr; QSocketNotifier *m_socketNotifier = nullptr; + QSocketNotifier *m_readyNotifier = nullptr; QTimer *m_resetCrashCountTimer = nullptr; - int m_display = -1; - QFutureWatcher *m_watcher = nullptr; ApplicationWaylandAbstract *m_app; QScopedPointer m_selectionOwner; QTemporaryFile m_authorityFile; + QScopedPointer m_socket; int m_crashCount = 0; Q_DISABLE_COPY(Xwayland) diff --git a/src/xwl/xwaylandsocket.cpp b/src/xwl/xwaylandsocket.cpp new file mode 100644 index 0000000000..23759104ae --- /dev/null +++ b/src/xwl/xwaylandsocket.cpp @@ -0,0 +1,211 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "xwaylandsocket.h" +#include "xwayland_logging.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ + +class UnixSocketAddress +{ +public: + enum class Type { + Unix, + Abstract, + }; + + UnixSocketAddress(const QString &socketPath, Type type); + + const sockaddr *data() const; + int size() const; + +private: + QByteArray m_buffer; +}; + +UnixSocketAddress::UnixSocketAddress(const QString &socketPath, Type type) +{ + const QByteArray encodedSocketPath = QFile::encodeName(socketPath); + + int byteCount = offsetof(sockaddr_un, sun_path) + encodedSocketPath.size(); + if (type == Type::Abstract) { + byteCount++; // For the first '\0'. + } + m_buffer.resize(byteCount); + + sockaddr_un *address = reinterpret_cast(m_buffer.data()); + address->sun_family = AF_UNIX; + + if (type == Type::Unix) { + qstrcpy(address->sun_path, encodedSocketPath); + } else { + *address->sun_path = '\0'; + qstrcpy(address->sun_path + 1, encodedSocketPath); + } +} + +const sockaddr *UnixSocketAddress::data() const +{ + return reinterpret_cast(m_buffer.data()); +} + +int UnixSocketAddress::size() const +{ + return m_buffer.size(); +} + +static QString lockFileNameForDisplay(int display) +{ + return QStringLiteral("/tmp/.X%1-lock").arg(display); +} + +static QString socketFileNameForDisplay(int display) +{ + return QStringLiteral("/tmp/.X11-unix/X%1").arg(display); +} + +static bool tryLockFile(const QString &lockFileName) +{ + for (int attempt = 0; attempt < 3; ++attempt) { + QFile lockFile(lockFileName); + if (lockFile.open(QFile::WriteOnly | QFile::NewOnly)) { + char buffer[12]; + snprintf(buffer, sizeof(buffer), "%10lld\n", QCoreApplication::applicationPid()); + if (lockFile.write(buffer, sizeof(buffer) - 1) != sizeof(buffer) - 1) { + qCWarning(KWIN_XWL) << "Failed to write pid to lock file:" << lockFile.errorString(); + lockFile.remove(); + return false; + } + return true; + } else if (lockFile.open(QFile::ReadOnly)) { + const int lockPid = lockFile.readLine().trimmed().toInt(); + if (!lockPid) { + return false; + } + if (kill(lockPid, 0) < 0 && errno == ESRCH) { + lockFile.remove(); // Try to grab the lock file in the next loop iteration. + } else { + return false; + } + } + } + return false; +} + +static int listen_helper(const QString &filePath, UnixSocketAddress::Type type) +{ + const UnixSocketAddress socketAddress(filePath, type); + + int fileDescriptor = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fileDescriptor == -1) { + return -1; + } + + if (bind(fileDescriptor, socketAddress.data(), socketAddress.size()) == -1) { + close(fileDescriptor); + return -1; + } + + if (listen(fileDescriptor, 1) == -1) { + close(fileDescriptor); + return -1; + } + + return fileDescriptor; +} + +XwaylandSocket::XwaylandSocket() +{ + QDir socketDirectory(QStringLiteral("/tmp/.X11-unix")); + if (!socketDirectory.exists()) { + socketDirectory.mkpath(QStringLiteral(".")); + } + + for (int display = 0; display < 100; ++display) { + const QString socketFilePath = socketFileNameForDisplay(display); + const QString lockFilePath = lockFileNameForDisplay(display); + + if (!tryLockFile(lockFilePath)) { + continue; + } + + const int unixFileDescriptor = listen_helper(socketFilePath, UnixSocketAddress::Type::Unix); + if (unixFileDescriptor == -1) { + QFile::remove(lockFilePath); + continue; + } + + const int abstractFileDescriptor = listen_helper(socketFilePath, UnixSocketAddress::Type::Abstract); + if (abstractFileDescriptor == -1) { + QFile::remove(lockFilePath); + QFile::remove(socketFilePath); + close(unixFileDescriptor); + continue; + } + + m_socketFilePath = socketFilePath; + m_lockFilePath = lockFilePath; + m_unixFileDescriptor = unixFileDescriptor; + m_abstractFileDescriptor = abstractFileDescriptor; + m_display = display; + return; + } + + qCWarning(KWIN_XWL) << "Failed to find free X11 connection socket"; +} + +XwaylandSocket::~XwaylandSocket() +{ + if (m_unixFileDescriptor != -1) { + close(m_unixFileDescriptor); + } + if (m_abstractFileDescriptor != -1) { + close(m_abstractFileDescriptor); + } + if (!m_socketFilePath.isEmpty()) { + QFile::remove(m_socketFilePath); + } + if (!m_lockFilePath.isEmpty()) { + QFile::remove(m_lockFilePath); + } +} + +bool XwaylandSocket::isValid() const +{ + return m_display != -1; +} + +int XwaylandSocket::unixFileDescriptor() const +{ + return m_unixFileDescriptor; +} + +int XwaylandSocket::abstractFileDescriptor() const +{ + return m_abstractFileDescriptor; +} + +int XwaylandSocket::display() const +{ + return m_display; +} + +QString XwaylandSocket::name() const +{ + return ":" + QString::number(m_display); +} + +} // namespace KWin diff --git a/src/xwl/xwaylandsocket.h b/src/xwl/xwaylandsocket.h new file mode 100644 index 0000000000..77c73cf27f --- /dev/null +++ b/src/xwl/xwaylandsocket.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace KWin +{ + +class XwaylandSocket +{ +public: + XwaylandSocket(); + ~XwaylandSocket(); + + bool isValid() const; + int display() const; + QString name() const; + + int unixFileDescriptor() const; + int abstractFileDescriptor() const; + +private: + int m_unixFileDescriptor = -1; + int m_abstractFileDescriptor = -1; + int m_display = -1; + QString m_socketFilePath; + QString m_lockFilePath; +}; + +} // namespace KWin