[education/rkward] /: Remove dbus dependency

Thomas Friedrichsmeier null at kde.org
Sun May 26 12:52:13 BST 2024


Git commit 8c70a223307d5a2514a3cdabcdb64ba174d59312 by Thomas Friedrichsmeier.
Committed on 26/05/2024 at 11:52.
Pushed by tfry into branch 'master'.

Remove dbus dependency

Base --reuse feature on KDSingleApplication, instead.

A  +26   -0    3rdparty/KDSingleApplication/CMakeLists.txt
A  +112  -0    3rdparty/KDSingleApplication/kdsingleapplication.cpp     [License: MIT]
A  +61   -0    3rdparty/KDSingleApplication/kdsingleapplication.h     [License: MIT]
A  +23   -0    3rdparty/KDSingleApplication/kdsingleapplication_lib.h     [License: MIT]
A  +339  -0    3rdparty/KDSingleApplication/kdsingleapplication_localsocket.cpp     [License: MIT]
A  +129  -0    3rdparty/KDSingleApplication/kdsingleapplication_localsocket_p.h     [License: MIT]
M  +2    -1    CMakeLists.txt
A  +7    -0    LICENSES/MIT.txt
M  +1    -1    rkward/CMakeLists.txt
M  +45   -26   rkward/main.cpp
M  +1    -2    rkward/misc/CMakeLists.txt
D  +0    -51   rkward/misc/rkdbusapi.cpp
D  +0    -25   rkward/misc/rkdbusapi.h
M  +0    -17   rkward/rkconsole.cpp
M  +0    -4    rkward/rkward.cpp

https://invent.kde.org/education/rkward/-/commit/8c70a223307d5a2514a3cdabcdb64ba174d59312

diff --git a/3rdparty/KDSingleApplication/CMakeLists.txt b/3rdparty/KDSingleApplication/CMakeLists.txt
new file mode 100644
index 000000000..63a3ec0a5
--- /dev/null
+++ b/3rdparty/KDSingleApplication/CMakeLists.txt
@@ -0,0 +1,26 @@
+# This file is part of KDSingleApplication.
+#
+# SPDX-FileCopyrightText: 2020-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info at kdab.com>
+#
+# SPDX-License-Identifier: MIT
+#
+# Contact KDAB at <info at kdab.com> for commercial licensing options.
+#
+
+# NOTE: severly stripped down from upstream version
+set(KDSINGLEAPPLICATION_SRCS kdsingleapplication.cpp kdsingleapplication_localsocket.cpp)
+
+add_library(kdsingleapplication STATIC ${KDSINGLEAPPLICATION_INSTALLABLE_INCLUDES} ${KDSINGLEAPPLICATION_SRCS})
+target_compile_definitions(kdsingleapplication PUBLIC KDSINGLEAPPLICATION_STATIC_BUILD)
+add_library(
+    KDAB::kdsingleapplication ALIAS kdsingleapplication
+)
+
+if(WIN32)
+    target_link_libraries(kdsingleapplication PRIVATE kernel32)
+endif()
+target_link_libraries(
+    kdsingleapplication
+    PUBLIC Qt::Core
+    PRIVATE Qt::Network
+)
diff --git a/3rdparty/KDSingleApplication/kdsingleapplication.cpp b/3rdparty/KDSingleApplication/kdsingleapplication.cpp
new file mode 100644
index 000000000..d0945bb66
--- /dev/null
+++ b/3rdparty/KDSingleApplication/kdsingleapplication.cpp
@@ -0,0 +1,112 @@
+/*
+  This file is part of KDSingleApplication.
+
+  SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info at kdab.com>
+
+  SPDX-License-Identifier: MIT
+
+  Contact KDAB at <info at kdab.com> for commercial licensing options.
+*/
+
+#include "kdsingleapplication.h"
+
+#include <QtCore/QCoreApplication>
+#include <QtCore/QFileInfo>
+
+// TODO: make this pluggable.
+#include "kdsingleapplication_localsocket_p.h"
+
+// Avoiding dragging in Qt private APIs for now, so this does not inherit
+// from QObjectPrivate.
+class KDSingleApplicationPrivate
+{
+public:
+    explicit KDSingleApplicationPrivate(const QString &name, KDSingleApplication::Options options, KDSingleApplication *q);
+
+    QString name() const
+    {
+        return m_name;
+    }
+
+    bool isPrimaryInstance() const
+    {
+        return m_impl.isPrimaryInstance();
+    }
+
+    bool sendMessage(const QByteArray &message, int timeout)
+    {
+        return m_impl.sendMessage(message, timeout);
+    }
+
+private:
+    Q_DECLARE_PUBLIC(KDSingleApplication)
+
+    KDSingleApplication *q_ptr;
+    QString m_name;
+
+    KDSingleApplicationLocalSocket m_impl;
+};
+
+KDSingleApplicationPrivate::KDSingleApplicationPrivate(const QString &name, KDSingleApplication::Options options, KDSingleApplication *q)
+    : q_ptr(q)
+    , m_name(name)
+    , m_impl(name, options)
+{
+    if (Q_UNLIKELY(name.isEmpty()))
+        qFatal("KDSingleApplication requires a non-empty application name");
+
+    if (isPrimaryInstance()) {
+        QObject::connect(&m_impl, &KDSingleApplicationLocalSocket::messageReceived,
+                         q, &KDSingleApplication::messageReceived);
+    }
+}
+
+static QString extractExecutableName(const QString &applicationFilePath)
+{
+    return QFileInfo(applicationFilePath).fileName();
+}
+
+KDSingleApplication::KDSingleApplication(QObject *parent)
+    : KDSingleApplication(extractExecutableName(QCoreApplication::applicationFilePath()), parent)
+{
+}
+
+KDSingleApplication::KDSingleApplication(const QString &name, QObject *parent)
+    : QObject(parent)
+    , d_ptr(new KDSingleApplicationPrivate(name, Option::IncludeUsernameInSocketName | Option::IncludeSessionInSocketName, this))
+{
+}
+
+KDSingleApplication::KDSingleApplication(const QString &name, Options options, QObject *parent)
+    : QObject(parent)
+    , d_ptr(new KDSingleApplicationPrivate(name, options, this))
+{
+}
+
+QString KDSingleApplication::name() const
+{
+    Q_D(const KDSingleApplication);
+    return d->name();
+}
+
+bool KDSingleApplication::isPrimaryInstance() const
+{
+    Q_D(const KDSingleApplication);
+    return d->isPrimaryInstance();
+}
+
+bool KDSingleApplication::sendMessage(const QByteArray &message)
+{
+    return sendMessageWithTimeout(message, 5000);
+}
+
+bool KDSingleApplication::sendMessageWithTimeout(const QByteArray &message, int timeout)
+{
+    Q_ASSERT(!isPrimaryInstance());
+
+    Q_D(KDSingleApplication);
+    return d->sendMessage(message, timeout);
+}
+
+
+KDSingleApplication::~KDSingleApplication() = default;
diff --git a/3rdparty/KDSingleApplication/kdsingleapplication.h b/3rdparty/KDSingleApplication/kdsingleapplication.h
new file mode 100644
index 000000000..e8cc35c76
--- /dev/null
+++ b/3rdparty/KDSingleApplication/kdsingleapplication.h
@@ -0,0 +1,61 @@
+/*
+  This file is part of KDSingleApplication.
+
+  SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info at kdab.com>
+
+  SPDX-License-Identifier: MIT
+
+  Contact KDAB at <info at kdab.com> for commercial licensing options.
+*/
+#ifndef KDSINGLEAPPLICATION_H
+#define KDSINGLEAPPLICATION_H
+
+#include <QtCore/QObject>
+#include <QtCore/QFlags>
+
+#include <memory>
+
+#include "kdsingleapplication_lib.h"
+
+class KDSingleApplicationPrivate;
+
+class KDSINGLEAPPLICATION_EXPORT KDSingleApplication : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString name READ name CONSTANT)
+    Q_PROPERTY(bool isPrimaryInstance READ isPrimaryInstance CONSTANT)
+
+public:
+    // IncludeUsernameInSocketName - Include the username in the socket name.
+    // IncludeSessionInSocketName - Include the graphical session in the socket name.
+    enum class Option {
+        None = 0x0,
+        IncludeUsernameInSocketName = 0x1,
+        IncludeSessionInSocketName = 0x2,
+    };
+    Q_DECLARE_FLAGS(Options, Option)
+
+    explicit KDSingleApplication(QObject *parent = nullptr);
+    explicit KDSingleApplication(const QString &name, QObject *parent = nullptr);
+    explicit KDSingleApplication(const QString &name, Options options, QObject *parent = nullptr);
+    ~KDSingleApplication();
+
+    QString name() const;
+    bool isPrimaryInstance() const;
+
+public Q_SLOTS:
+    // avoid default arguments and overloads, as they don't mix with connections
+    bool sendMessage(const QByteArray &message);
+    bool sendMessageWithTimeout(const QByteArray &message, int timeout);
+
+Q_SIGNALS:
+    void messageReceived(const QByteArray &message);
+
+private:
+    Q_DECLARE_PRIVATE(KDSingleApplication)
+    std::unique_ptr<KDSingleApplicationPrivate> d_ptr;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(KDSingleApplication::Options)
+
+#endif // KDSINGLEAPPLICATION_H
diff --git a/3rdparty/KDSingleApplication/kdsingleapplication_lib.h b/3rdparty/KDSingleApplication/kdsingleapplication_lib.h
new file mode 100644
index 000000000..060dd6739
--- /dev/null
+++ b/3rdparty/KDSingleApplication/kdsingleapplication_lib.h
@@ -0,0 +1,23 @@
+/*
+  This file is part of KDSingleApplication.
+
+  SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info at kdab.com>
+
+  SPDX-License-Identifier: MIT
+
+  Contact KDAB at <info at kdab.com> for commercial licensing options.
+*/
+#ifndef KDSINGLEAPPLICATION_LIB_H
+#define KDSINGLEAPPLICATION_LIB_H
+
+#include <QtCore/QtGlobal>
+
+#if defined(KDSINGLEAPPLICATION_STATIC_BUILD)
+#define KDSINGLEAPPLICATION_EXPORT
+#elif defined(KDSINGLEAPPLICATION_SHARED_BUILD)
+#define KDSINGLEAPPLICATION_EXPORT Q_DECL_EXPORT
+#else
+#define KDSINGLEAPPLICATION_EXPORT Q_DECL_IMPORT
+#endif
+
+#endif // KDSINGLEAPPLICATION_LIB_H
diff --git a/3rdparty/KDSingleApplication/kdsingleapplication_localsocket.cpp b/3rdparty/KDSingleApplication/kdsingleapplication_localsocket.cpp
new file mode 100644
index 000000000..8b3b63230
--- /dev/null
+++ b/3rdparty/KDSingleApplication/kdsingleapplication_localsocket.cpp
@@ -0,0 +1,339 @@
+/*
+  This file is part of KDSingleApplication.
+
+  SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info at kdab.com>
+
+  SPDX-License-Identifier: MIT
+
+  Contact KDAB at <info at kdab.com> for commercial licensing options.
+*/
+
+#include "kdsingleapplication_localsocket_p.h"
+
+#include <QtCore/QDir>
+#include <QtCore/QDeadlineTimer>
+#include <QtCore/QTimer>
+#include <QtCore/QLockFile>
+#include <QtCore/QDataStream>
+
+#include <QtCore/QtDebug>
+#include <QtCore/QLoggingCategory>
+
+#include <QtNetwork/QLocalServer>
+#include <QtNetwork/QLocalSocket>
+
+#include <chrono>
+#include <algorithm>
+
+#if defined(Q_OS_UNIX)
+// for ::getuid()
+#include <sys/types.h>
+#include <unistd.h>
+#include <pwd.h>
+#endif
+
+#if defined(Q_OS_WIN)
+#include <qt_windows.h>
+#include <lmcons.h>
+#endif
+
+#include "kdsingleapplication.h"
+
+static const auto LOCALSOCKET_CONNECTION_TIMEOUT = std::chrono::seconds(5);
+static const char LOCALSOCKET_PROTOCOL_VERSION = 2;
+
+Q_LOGGING_CATEGORY(kdsaLocalSocket, "kdsingleapplication.localsocket", QtWarningMsg);
+
+KDSingleApplicationLocalSocket::KDSingleApplicationLocalSocket(const QString &name, KDSingleApplication::Options options, QObject *parent)
+    : QObject(parent)
+{
+    /* cppcheck-suppress useInitializationList */
+    m_socketName = QStringLiteral("kdsingleapp");
+
+#if defined(Q_OS_UNIX)
+    if (options.testFlag(KDSingleApplication::Option::IncludeUsernameInSocketName)) {
+        m_socketName += QStringLiteral("-");
+        uid_t uid = ::getuid();
+        struct passwd *pw = ::getpwuid(uid);
+        if (pw) {
+            QString username = QString::fromUtf8(pw->pw_name);
+            m_socketName += username;
+        } else {
+            m_socketName += QString::number(uid);
+        }
+    }
+    if (options.testFlag(KDSingleApplication::Option::IncludeSessionInSocketName)) {
+        QString sessionId = qEnvironmentVariable("XDG_SESSION_ID");
+        if (!sessionId.isEmpty()) {
+            m_socketName += QStringLiteral("-");
+            m_socketName += sessionId;
+        }
+    }
+#elif defined(Q_OS_WIN)
+    // I'm not sure of a "global session identifier" on Windows; are
+    // multiple logins from the same user a possibility? For now, following this:
+    // https://docs.microsoft.com/en-us/windows/desktop/devnotes/getting-the-session-id-of-the-current-process
+    if (options.testFlag(KDSingleApplication::Option::IncludeUsernameInSocketName)) {
+        DWORD usernameLen = UNLEN + 1;
+        wchar_t username[UNLEN + 1];
+        if (GetUserNameW(username, &usernameLen)) {
+            m_socketName += QStringLiteral("-");
+            m_socketName += QString::fromWCharArray(username);
+        }
+    }
+    if (options.testFlag(KDSingleApplication::Option::IncludeSessionInSocketName)) {
+        DWORD sessionId;
+        BOOL haveSessionId = ProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
+        if (haveSessionId) {
+            m_socketName += QStringLiteral("-");
+            m_socketName += QString::number(sessionId);
+        }
+    }
+#else
+#error "KDSingleApplication has not been ported to this platform"
+#endif
+
+    m_socketName += QStringLiteral("-");
+    m_socketName += name;
+
+    const QString lockFilePath =
+        QDir::tempPath() + QLatin1Char('/') + m_socketName + QLatin1String(".lock");
+
+    qCDebug(kdsaLocalSocket) << "Socket name is" << m_socketName;
+    qCDebug(kdsaLocalSocket) << "Lock file path is" << lockFilePath;
+
+    std::unique_ptr<QLockFile> lockFile(new QLockFile(lockFilePath));
+    lockFile->setStaleLockTime(0);
+
+    if (!lockFile->tryLock()) {
+        // someone else has the lock => we're secondary
+        qCDebug(kdsaLocalSocket) << "Secondary instance";
+        return;
+    }
+
+    qCDebug(kdsaLocalSocket) << "Primary instance";
+
+    std::unique_ptr<QLocalServer> server = std::make_unique<QLocalServer>();
+    if (!server->listen(m_socketName)) {
+        // maybe the primary crashed, leaving a stale socket; delete it and try again
+        QLocalServer::removeServer(m_socketName);
+        if (!server->listen(m_socketName)) {
+            // TODO: better error handling.
+            qWarning("KDSingleApplication: unable to make the primary instance listen on %ls: %ls",
+                     qUtf16Printable(m_socketName),
+                     qUtf16Printable(server->errorString()));
+
+            return;
+        }
+    }
+
+    connect(server.get(), &QLocalServer::newConnection,
+            this, &KDSingleApplicationLocalSocket::handleNewConnection);
+
+    m_lockFile = std::move(lockFile);
+    m_localServer = std::move(server);
+}
+
+KDSingleApplicationLocalSocket::~KDSingleApplicationLocalSocket() = default;
+
+bool KDSingleApplicationLocalSocket::isPrimaryInstance() const
+{
+    return m_localServer != nullptr;
+}
+
+bool KDSingleApplicationLocalSocket::sendMessage(const QByteArray &message, int timeout)
+{
+    Q_ASSERT(!isPrimaryInstance());
+    QLocalSocket socket;
+
+    qCDebug(kdsaLocalSocket) << "Preparing to send message" << message << "with timeout" << timeout;
+
+    QDeadlineTimer deadline(timeout);
+
+    // There is an inherent race here with the setup of the server side.
+    // Even if the socket lock is held by the server, the server may not
+    // be listening yet. So this connection may fail; keep retrying
+    // until we hit the timeout.
+    do {
+        socket.connectToServer(m_socketName);
+        if (socket.waitForConnected(deadline.remainingTime()))
+            break;
+    } while (!deadline.hasExpired());
+
+    qCDebug(kdsaLocalSocket) << "Socket state:" << socket.state() << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
+
+    if (deadline.hasExpired()) {
+        qCWarning(kdsaLocalSocket) << "Connection timed out";
+        return false;
+    }
+
+    socket.write(&LOCALSOCKET_PROTOCOL_VERSION, 1);
+
+    {
+        QByteArray encodedMessage;
+        QDataStream ds(&encodedMessage, QIODevice::WriteOnly);
+        ds << message;
+        socket.write(encodedMessage);
+    }
+
+    qCDebug(kdsaLocalSocket) << "Wrote message in the socket"
+                             << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
+
+    // There is no acknowledgement mechanism here.
+    // Should there be one?
+
+    while (socket.bytesToWrite() > 0) {
+        if (!socket.waitForBytesWritten(deadline.remainingTime())) {
+            qCWarning(kdsaLocalSocket) << "Message to primary timed out";
+            return false;
+        }
+    }
+
+    qCDebug(kdsaLocalSocket) << "Bytes written, now disconnecting"
+                             << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
+
+    socket.disconnectFromServer();
+
+    if (socket.state() == QLocalSocket::UnconnectedState) {
+        qCDebug(kdsaLocalSocket) << "Disconnected -- success!";
+        return true;
+    }
+
+    if (!socket.waitForDisconnected(deadline.remainingTime())) {
+        qCWarning(kdsaLocalSocket) << "Disconnection from primary timed out";
+        return false;
+    }
+
+    qCDebug(kdsaLocalSocket) << "Disconnected -- success!";
+
+    return true;
+}
+
+void KDSingleApplicationLocalSocket::handleNewConnection()
+{
+    Q_ASSERT(m_localServer);
+
+    QLocalSocket *socket;
+    while ((socket = m_localServer->nextPendingConnection())) {
+        qCDebug(kdsaLocalSocket) << "Got new connection on" << m_socketName << "state" << socket->state();
+
+        Connection c(socket);
+        socket = c.socket.get();
+
+        c.readDataConnection = QObjectConnectionHolder(
+            connect(socket, &QLocalSocket::readyRead,
+                    this, &KDSingleApplicationLocalSocket::readDataFromSecondary));
+
+        c.secondaryDisconnectedConnection = QObjectConnectionHolder(
+            connect(socket, &QLocalSocket::disconnected,
+                    this, &KDSingleApplicationLocalSocket::secondaryDisconnected));
+
+        c.abortConnection = QObjectConnectionHolder(
+            connect(c.timeoutTimer.get(), &QTimer::timeout,
+                    this, &KDSingleApplicationLocalSocket::abortConnectionToSecondary));
+
+        m_clients.push_back(std::move(c));
+
+        // Note that by the time we get here, the socket could've already been closed,
+        // and no signals emitted (hello, Windows!). Read what's already in the socket.
+        if (readDataFromSecondarySocket(socket))
+            return;
+
+        if (socket->state() == QLocalSocket::UnconnectedState)
+            secondarySocketDisconnected(socket);
+    }
+}
+
+template<typename Container>
+static auto findConnectionBySocket(Container &container, QLocalSocket *socket)
+{
+    auto i = std::find_if(container.begin(),
+                          container.end(),
+                          [socket](const auto &c) { return c.socket.get() == socket; });
+    Q_ASSERT(i != container.end());
+    return i;
+}
+
+template<typename Container>
+static auto findConnectionByTimer(Container &container, QTimer *timer)
+{
+    auto i = std::find_if(container.begin(),
+                          container.end(),
+                          [timer](const auto &c) { return c.timeoutTimer.get() == timer; });
+    Q_ASSERT(i != container.end());
+    return i;
+}
+
+void KDSingleApplicationLocalSocket::readDataFromSecondary()
+{
+    QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
+    readDataFromSecondarySocket(socket);
+}
+
+bool KDSingleApplicationLocalSocket::readDataFromSecondarySocket(QLocalSocket *socket)
+{
+    auto i = findConnectionBySocket(m_clients, socket);
+    Connection &c = *i;
+    c.readData.append(socket->readAll());
+
+    qCDebug(kdsaLocalSocket) << "Got more data from a secondary. Data read so far:" << c.readData;
+
+    const QByteArray &data = c.readData;
+
+    if (data.size() >= 1) {
+        if (data[0] != LOCALSOCKET_PROTOCOL_VERSION) {
+            qCDebug(kdsaLocalSocket) << "Got an invalid protocol version";
+            m_clients.erase(i);
+            return true;
+        }
+    }
+
+    QDataStream ds(data);
+    ds.skipRawData(1);
+
+    ds.startTransaction();
+    QByteArray message;
+    ds >> message;
+
+    if (ds.commitTransaction()) {
+        qCDebug(kdsaLocalSocket) << "Got a complete message:" << message;
+        Q_EMIT messageReceived(message);
+        m_clients.erase(i);
+        return true;
+    }
+
+    return false;
+}
+
+void KDSingleApplicationLocalSocket::secondaryDisconnected()
+{
+    QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
+    secondarySocketDisconnected(socket);
+}
+
+void KDSingleApplicationLocalSocket::secondarySocketDisconnected(QLocalSocket *socket)
+{
+    auto i = findConnectionBySocket(m_clients, socket);
+    Connection c = std::move(*i);
+    m_clients.erase(i);
+
+    qCDebug(kdsaLocalSocket) << "Secondary disconnected. Data read:" << c.readData;
+}
+
+void KDSingleApplicationLocalSocket::abortConnectionToSecondary()
+{
+    QTimer *timer = static_cast<QTimer *>(sender());
+
+    auto i = findConnectionByTimer(m_clients, timer);
+    Connection c = std::move(*i);
+    m_clients.erase(i);
+
+    qCDebug(kdsaLocalSocket) << "Secondary timed out. Data read:" << c.readData;
+}
+
+KDSingleApplicationLocalSocket::Connection::Connection(QLocalSocket *_socket)
+    : socket(_socket)
+    , timeoutTimer(new QTimer)
+{
+    timeoutTimer->start(LOCALSOCKET_CONNECTION_TIMEOUT);
+}
diff --git a/3rdparty/KDSingleApplication/kdsingleapplication_localsocket_p.h b/3rdparty/KDSingleApplication/kdsingleapplication_localsocket_p.h
new file mode 100644
index 000000000..2c586160b
--- /dev/null
+++ b/3rdparty/KDSingleApplication/kdsingleapplication_localsocket_p.h
@@ -0,0 +1,129 @@
+/*
+  This file is part of KDSingleApplication.
+
+  SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info at kdab.com>
+
+  SPDX-License-Identifier: MIT
+
+  Contact KDAB at <info at kdab.com> for commercial licensing options.
+*/
+
+#ifndef KDSINGLEAPPLICATION_LOCALSOCKET_P_H
+#define KDSINGLEAPPLICATION_LOCALSOCKET_P_H
+
+#include <QtCore/QObject>
+#include <QtCore/QByteArray>
+#include <QtCore/QString>
+
+QT_BEGIN_NAMESPACE
+class QLockFile;
+class QLocalServer;
+class QLocalSocket;
+class QTimer;
+QT_END_NAMESPACE
+
+#include <memory>
+#include <vector>
+
+#include "kdsingleapplication.h"
+
+struct QObjectDeleteLater
+{
+    void operator()(QObject *o)
+    {
+        o->deleteLater();
+    }
+};
+
+class QObjectConnectionHolder
+{
+    Q_DISABLE_COPY(QObjectConnectionHolder)
+    QMetaObject::Connection c;
+
+public:
+    QObjectConnectionHolder()
+    {
+    }
+
+    explicit QObjectConnectionHolder(QMetaObject::Connection _c)
+        : c(std::move(_c))
+    {
+    }
+
+    ~QObjectConnectionHolder()
+    {
+        QObject::disconnect(c);
+    }
+
+    QObjectConnectionHolder(QObjectConnectionHolder &&other) noexcept
+        : c(std::exchange(other.c, {}))
+    {
+    }
+
+    QObjectConnectionHolder &operator=(QObjectConnectionHolder &&other) noexcept
+    {
+        QObjectConnectionHolder moved(std::move(other));
+        swap(moved);
+        return *this;
+    }
+
+    void swap(QObjectConnectionHolder &other) noexcept
+    {
+        using std::swap;
+        swap(c, other.c);
+    }
+};
+
+class KDSingleApplicationLocalSocket : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit KDSingleApplicationLocalSocket(const QString &name,
+                                            KDSingleApplication::Options options,
+                                            QObject *parent = nullptr);
+    ~KDSingleApplicationLocalSocket();
+
+    bool isPrimaryInstance() const;
+
+public Q_SLOTS:
+    bool sendMessage(const QByteArray &message, int timeout);
+
+Q_SIGNALS:
+    void messageReceived(const QByteArray &message);
+
+private:
+    void handleNewConnection();
+    void readDataFromSecondary();
+    bool readDataFromSecondarySocket(QLocalSocket *socket);
+    void secondaryDisconnected();
+    void secondarySocketDisconnected(QLocalSocket *socket);
+    void abortConnectionToSecondary();
+
+    QString m_socketName;
+
+    std::unique_ptr<QLockFile> m_lockFile; // protects m_localServer
+    std::unique_ptr<QLocalServer> m_localServer;
+
+    struct Connection
+    {
+        explicit Connection(QLocalSocket *s);
+
+        std::unique_ptr<QLocalSocket, QObjectDeleteLater> socket;
+        std::unique_ptr<QTimer, QObjectDeleteLater> timeoutTimer;
+        QByteArray readData;
+
+        // socket/timeoutTimer are deleted via deleteLater (as we delete them
+        // in slots connected to their signals). Before the deleteLater is acted upon,
+        // they may emit further signals, triggering logic that it's not supposed
+        // to be triggered (as the Connection has already been destroyed).
+        // Use this Holder to break the connections.
+        QObjectConnectionHolder readDataConnection;
+        QObjectConnectionHolder secondaryDisconnectedConnection;
+        QObjectConnectionHolder abortConnection;
+    };
+
+    std::vector<Connection> m_clients;
+};
+
+#endif // KDSINGLEAPPLICATION_LOCALSOCKET_P_H
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c1d84bdce..802ef0169 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,7 +22,7 @@ INCLUDE(ECMAddAppIcon)
 INCLUDE(ECMMarkNonGuiExecutable)
 INCLUDE(FeatureSummary)
 
-FIND_PACKAGE(Qt6 6.6 CONFIG REQUIRED COMPONENTS Widgets Core Xml Network Qml PrintSupport WebEngineWidgets Core5Compat DBus) # TODO: remove compat
+FIND_PACKAGE(Qt6 6.6 CONFIG REQUIRED COMPONENTS Widgets Core Xml Network Qml PrintSupport WebEngineWidgets Core5Compat) # TODO: remove compat
 FIND_PACKAGE(KF6 6.0.0 REQUIRED COMPONENTS CoreAddons DocTools I18n XmlGui TextEditor WidgetsAddons Parts Config Notifications WindowSystem Archive OPTIONAL_COMPONENTS Crash)
 FIND_PACKAGE(Gettext REQUIRED)
 
@@ -37,6 +37,7 @@ remove_definitions(-DQT_NO_CAST_FROM_ASCII) # TODO remove to compley to KDECompi
 #uncomment the line below to save ~250-350kB in object size
 #ADD_DEFINITIONS(-DRKWARD_NO_TRACE)
 
+ADD_SUBDIRECTORY(3rdparty/KDSingleApplication)
 ADD_SUBDIRECTORY(rkward)
 ADD_SUBDIRECTORY(doc)
 ADD_SUBDIRECTORY(tests)
diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt
new file mode 100644
index 000000000..2942df207
--- /dev/null
+++ b/LICENSES/MIT.txt
@@ -0,0 +1,7 @@
+Copyright <YEAR> <COPYRIGHT HOLDER>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/rkward/CMakeLists.txt b/rkward/CMakeLists.txt
index ae8b7045e..1ab4e48b8 100644
--- a/rkward/CMakeLists.txt
+++ b/rkward/CMakeLists.txt
@@ -43,7 +43,7 @@ LINK_DIRECTORIES(${R_SHAREDLIBDIR})
 
 ADD_LIBRARY(rkward_lib STATIC ${RKWard_Lib_Sources})
 TARGET_COMPILE_DEFINITIONS(rkward_lib PUBLIC -DR_EXECUTABLE="${R_EXECUTABLE}")
-TARGET_LINK_LIBRARIES(rkward_lib windows ${RKWARD_ADDLIBS} agents dialogs plugin settings dataeditor core scriptbackends rbackend misc KF6::WindowSystem Qt6::Widgets KF6::XmlGui)
+TARGET_LINK_LIBRARIES(rkward_lib windows ${RKWARD_ADDLIBS} agents dialogs plugin settings dataeditor core scriptbackends rbackend misc KDAB::kdsingleapplication KF6::WindowSystem Qt6::Widgets KF6::XmlGui)
 
 SET(RKWard_App_Sources
 	main.cpp
diff --git a/rkward/main.cpp b/rkward/main.cpp
index 7c3fcbf68..000e28fb2 100644
--- a/rkward/main.cpp
+++ b/rkward/main.cpp
@@ -48,6 +48,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #if __has_include(<KStartupInfo>)
 #	include <KStartupInfo>
 #endif
+#include <KWindowSystem>
 #ifdef WITH_KCRASH
 #	include <KCrash>
 #endif
@@ -60,9 +61,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <QApplication>
 #include <QUrl>
 #include <QCommandLineParser>
-#include <QDBusConnection>
-#include <QDBusInterface>
-#include <QDBusReply>
 #include <QTime>
 #include <QSettings>
 #include <QStandardPaths>
@@ -83,8 +81,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include "settings/rksettingsmodulegeneral.h"
 #include "rbackend/rksessionvars.h"
 #include "windows/rkdebugmessagewindow.h"
-#include "misc/rkdbusapi.h"
 #include "misc/rkcommonfunctions.h"
+#include "../3rdparty/KDSingleApplication/kdsingleapplication.h"
 
 #ifdef Q_OS_WIN
 	// these are needed for the exit hack.
@@ -233,10 +231,11 @@ int main (int argc, char *argv[]) {
 	RK_Debug::RK_Debug_Level = DL_WARNING;
 	// annoyingly, QWebEngineUrlSchemes have to be registered before creating the app.
 	QWebEngineUrlScheme scheme("help");
-	scheme.setSyntax (QWebEngineUrlScheme::Syntax::Path);
-	scheme.setFlags (QWebEngineUrlScheme::LocalScheme|QWebEngineUrlScheme::LocalAccessAllowed);
-	QWebEngineUrlScheme::registerScheme (scheme);
-	QApplication app (argc, argv);
+	scheme.setSyntax(QWebEngineUrlScheme::Syntax::Path);
+	scheme.setFlags(QWebEngineUrlScheme::LocalScheme|QWebEngineUrlScheme::LocalAccessAllowed);
+	QWebEngineUrlScheme::registerScheme(scheme);
+	QApplication app(argc, argv);
+	KDSingleApplication app_singleton;
 #ifdef WITH_KCRASH
 	KCrash::setDrKonqiEnabled (true);
 #endif
@@ -333,10 +332,6 @@ int main (int argc, char *argv[]) {
 		qputenv ("PATH", QString ("%1/bin:%1/sbin:%2").arg (INSTALL_PATH).arg (oldpath).toLocal8Bit ());
 		if (RK_Debug::RK_Debug_Level > 3) qDebug ("Adjusting system path to %s", qPrintable (qgetenv ("PATH")));
 	}
-	if (!qEnvironmentVariableIsSet ("DBUS_LAUNCHD_SESSION_BUS_SOCKET")) {
-		// try to ensure that DBus is running before trying to connect
-		QProcess::execute ("launchctl", QStringList () << "load" << "/Library/LaunchAgents/org.freedesktop.dbus-session.plist");
-	}
 #elif defined(Q_OS_UNIX)
 	QStringList data_dirs = QString(qgetenv("XDG_DATA_DIRS")).split(PATH_VAR_SEP);;
 	QString reldatadir = QDir::cleanPath(QDir(app.applicationDirPath()).absoluteFilePath(REL_PATH_TO_DATA));
@@ -353,23 +348,20 @@ int main (int argc, char *argv[]) {
 		qputenv("PATH", syspaths.join(PATH_VAR_SEP).toLocal8Bit());
 	}
 
-	// Handle --reuse option, by placing a dbus-call to existing RKWard process (if any) and exiting
+	// Handle --reuse option, by forwarding url arguments to existing RKWard process (if any) and exiting
 	if (parser.isSet ("reuse") || (parser.isSet ("autoreuse") && !url_args.isEmpty ())) {
-		if (!QDBusConnection::sessionBus ().isConnected ()) {
-			RK_DEBUG (DEBUG_ALL, DL_WARNING, "Could not connect to session dbus");
-		} else {
-			QDBusInterface iface (RKDBUS_SERVICENAME, "/", "", QDBusConnection::sessionBus ());
-			if (iface.isValid ()) {
-				QDBusReply<void> reply = iface.call ("openAnyUrl", url_args, !parser.isSet ("nowarn-external"));
-				if (!reply.isValid ()) {
-					RK_DEBUG (DEBUG_ALL, DL_ERROR, "Error while placing dbus call: %s", qPrintable (reply.error ().message ()));
-					return 1;
-				}
+		if (!app_singleton.isPrimaryInstance()) {
+			QByteArray call;
+			QDataStream stream(&call, QIODevice::WriteOnly);
+			// TODO: nowarn-external
+			stream << QVariant(QStringLiteral("openAnyUrl")) << QVariant(url_args) << QVariant(parser.isSet("nowarn-external"));
+			app_singleton.sendMessageWithTimeout(call, 1000);
+			// TODO: should always debug to terminal in this case!
+			RK_DEBUG (DEBUG_ALL, DL_INFO, "Reusing running instance");
 #if __has_include(<KStartupInfo>)
-				KStartupInfo::appStarted();
+			KStartupInfo::appStarted();
 #endif
-				return 0;
-			}
+			return 0;
 		}
 	}
 
@@ -408,6 +400,33 @@ int main (int argc, char *argv[]) {
 		new RKWardMainWindow ();
 	}
 
+	if (app_singleton.isPrimaryInstance()) {
+		QObject::connect(&app_singleton, &KDSingleApplication::messageReceived, RKWardMainWindow::getMain(), [](const QByteArray &_message) {
+			// ok, raising the app window is totally hard to do, reliably. This solution copied from kate.
+			auto *main = RKWardMainWindow::getMain();
+			main->show();
+			main->activateWindow();
+			main->raise();
+			// [omitting activation token]
+			KWindowSystem::activateWindow(main->windowHandle());
+			// end
+
+			QByteArray message = _message;
+			QDataStream stream(&message, QIODevice::ReadOnly);
+			QVariant call;
+			QVariant urls;
+			QVariant nowarn;
+			stream >> call;
+			stream >> urls;
+			stream >> nowarn;
+			if (call == QStringLiteral("openAnyUrl")) {
+				main->openUrlsFromCommandLineOrDBus(!nowarn.toBool(), urls.toStringList());
+			} else {
+				RK_DEBUG (APP, DL_ERROR, "Unrecognized SingleApplication call");
+			}
+		});
+	}
+
 	// do it!
 	int status = app.exec ();
 
diff --git a/rkward/misc/CMakeLists.txt b/rkward/misc/CMakeLists.txt
index 5a30a8e0c..7575d7303 100644
--- a/rkward/misc/CMakeLists.txt
+++ b/rkward/misc/CMakeLists.txt
@@ -25,7 +25,6 @@ SET(misc_STAT_SRCS
    editlabelsdialog.cpp
    editformatdialog.cpp
    rkmessagecatalog.cpp
-   rkdbusapi.cpp
    rkfindbar.cpp
    rkdynamicsearchline.cpp
    rkaccordiontable.cpp
@@ -37,4 +36,4 @@ SET(misc_STAT_SRCS
    )
 
 ADD_LIBRARY(misc STATIC ${misc_STAT_SRCS})
-TARGET_LINK_LIBRARIES(misc Qt6::Widgets KF6::WidgetsAddons KF6::KIOWidgets Qt6::Xml Qt6::DBus KF6::ConfigCore KF6::Parts KF6::WindowSystem KF6::TextEditor KF6::Archive KF6::I18n)
+TARGET_LINK_LIBRARIES(misc Qt6::Widgets KF6::WidgetsAddons KF6::KIOWidgets Qt6::Xml KF6::ConfigCore KF6::Parts KF6::WindowSystem KF6::TextEditor KF6::Archive KF6::I18n)
diff --git a/rkward/misc/rkdbusapi.cpp b/rkward/misc/rkdbusapi.cpp
deleted file mode 100644
index a3e635d36..000000000
--- a/rkward/misc/rkdbusapi.cpp
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-rkdbusapi - This file is part of RKWard (https://rkward.kde.org). Created: Thu Nov 20 2014
-SPDX-FileCopyrightText: 2014-2015 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
-SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
-SPDX-License-Identifier: GPL-2.0-or-later
-*/
-
-#include "rkdbusapi.h"
-
-#include <QDBusConnection>
-#include <KWindowSystem>
-#if __has_include(<KStartupInfo>)
-#include <KStartupInfo>
-#endif
-#include "../rkward.h"
-
-#include "../debug.h"
-
-RKDBusAPI::RKDBusAPI (QObject* parent): QObject (parent) {
-	RK_TRACE (APP);
-
-	if (!QDBusConnection::sessionBus ().isConnected ()) {
-		RK_DEBUG (DL_ERROR, APP, "D-Bus session bus in not connected");
-		return;
-	}
-
-	if (!QDBusConnection::sessionBus ().registerService (RKDBUS_SERVICENAME)) {
-		RK_DEBUG (DL_ERROR, APP, "Could not register org.kde.rkward on session bus");
-		return;
-	}
-
-	QDBusConnection::sessionBus ().registerObject ("/", this, QDBusConnection::ExportScriptableSlots);
-}
-
-void RKDBusAPI::openAnyUrl (const QStringList& urls, bool warn_external) {
-	RK_TRACE (APP);
-
-	// ok, raising the app window is totally hard to do, reliably. This solution copied from kate.
-	QWidget *main = RKWardMainWindow::getMain ();
-	main->show();
-	main->activateWindow();
-	main->raise();
-
-	// omitting activation token
-
-	KWindowSystem::activateWindow(main->windowHandle());
-	// end
-
-	RKWardMainWindow::getMain ()->openUrlsFromCommandLineOrDBus (warn_external, urls);
-}
-
diff --git a/rkward/misc/rkdbusapi.h b/rkward/misc/rkdbusapi.h
deleted file mode 100644
index 7116ae88b..000000000
--- a/rkward/misc/rkdbusapi.h
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-rkdbusapi - This file is part of the RKWard project. Created: Thu Nov 20 2014
-SPDX-FileCopyrightText: 2014-2015 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
-SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
-SPDX-License-Identifier: GPL-2.0-or-later
-*/
-
-#ifndef RKDBUSAPI_H
-#define RKDBUSAPI_H
-
-#define RKDBUS_SERVICENAME "org.kde.rkward.api"
-
-#include <QObject>
-
-class RKDBusAPI : public QObject {
-	Q_OBJECT
-public:
-/** Creates an object (should be a singleton) to relay incoming DBus calls, and registers it on the session bus. */
-	explicit RKDBusAPI (QObject *parent);
-	~RKDBusAPI () {};
-public Q_SLOTS:
-	Q_SCRIPTABLE void openAnyUrl (const QStringList &urls, bool warn_external=true);
-};
-
-#endif
diff --git a/rkward/rkconsole.cpp b/rkward/rkconsole.cpp
index f9e4e35de..0440aa700 100644
--- a/rkward/rkconsole.cpp
+++ b/rkward/rkconsole.cpp
@@ -64,24 +64,7 @@ RKConsole::RKConsole (QWidget *parent, bool tool_window, const char *name) : RKM
 	QVBoxLayout *layout = new QVBoxLayout (this);
 	layout->setContentsMargins (0, 0, 0, 0);
 
-	// I'd like to have this as a runtime version-check, but for that, I'd have to create a KTextEditor::Editor::instance(), first, and by
-	// that time the katepart has already enumerated its highlighting files, and the hack below would not work (for this session, at least).
-	if (KTEXTEDITOR_VERSION < ((5 << 16) | (16 << 8))) {
-		// Older katepart5 (before 5.16.0) will not accept rkward.xml installed in its own directory (as that has an index.json where
-		// rkward.xml is not included). Thus, we try to sneak in rkward.xml as a local user syntax highlighting file.
-		QDir writable_path = QDir (QDir (QStandardPaths::writableLocation (QStandardPaths::GenericDataLocation)).absoluteFilePath ("katepart5/syntax"));
-		QFileInfo rkwardxml (writable_path.absoluteFilePath ("rkward.xml"));
-		if (!rkwardxml.exists ()) {
-			writable_path.mkpath (".");
-			QFile rkwardxml_up (QStandardPaths::locate (QStandardPaths::GenericDataLocation, "katepart5/syntax/rkward.xml"));
-			rkwardxml_up.copy (rkwardxml.absoluteFilePath ());
-			QFile index_json (writable_path.absoluteFilePath ("index.json"));
-			if (index_json.exists ()) index_json.remove ();
-		}
-	}
-
 	// create a Kate-part as command-editor
-	// KF5 TODO: (How) can we make sure we are getting a katepart, here, not some other implementation. Or can we take that for granted?
 	KTextEditor::Editor* editor = KTextEditor::Editor::instance ();
 	if (!editor) {
 		RK_ASSERT (false);
diff --git a/rkward/rkward.cpp b/rkward/rkward.cpp
index 7f394f924..ac1628b80 100644
--- a/rkward/rkward.cpp
+++ b/rkward/rkward.cpp
@@ -55,7 +55,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include "misc/rkstandardicons.h"
 #include "misc/rkcommonfunctions.h"
 #include "misc/rkxmlguisyncer.h"
-#include "misc/rkdbusapi.h"
 #include "misc/rkdialogbuttonbox.h"
 #include "misc/rkstyle.h"
 #include "dialogs/rkloadlibsdialog.h"
@@ -278,9 +277,6 @@ void RKWardMainWindow::doPostInit () {
 	RInterface::issueCommand (command);
 
 	if (!evaluate_code.isEmpty ()) RKConsole::pipeUserCommand (evaluate_code);
-	RKDBusAPI *dbus = new RKDBusAPI (this);
-	connect (this, &RKWardMainWindow::aboutToQuitRKWard, dbus, &RKDBusAPI::deleteLater);
-	// around on the bus in this case.
 
 	updateCWD ();
 	connect (RInterface::instance(), &RInterface::backendWorkdirChanged, this, &RKWardMainWindow::updateCWD);


More information about the rkward-tracker mailing list