Replace SingleApplication with KDSingleApplication

This commit is contained in:
Jonas Kvinge
2023-05-03 23:05:21 +02:00
parent b861703dad
commit 919ff414e6
27 changed files with 703 additions and 1562 deletions

View File

@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.7)
set(SOURCES kdsingleapplication.cpp kdsingleapplication_localsocket.cpp)
set(HEADERS kdsingleapplication.h kdsingleapplication_localsocket_p.h)
qt_wrap_cpp(MOC ${HEADERS})
add_library(kdsingleapplication STATIC ${SOURCES} ${MOC})
target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD)
target_include_directories(kdsingleapplication PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(kdsingleapplication PUBLIC ${QtCore_LIBRARIES} ${QtNetwork_LIBRARIES} )

6
3rdparty/kdsingleapplication/LICENSE vendored Normal file
View File

@@ -0,0 +1,6 @@
KDSingleApplication is (C) 2019-2023, Klarälvdalens Datakonsult AB,
and is available under the terms of the MIT license.
See the full license text in the LICENSES folder.
Contact KDAB at <info@kdab.com> to inquire about commercial licensing.

53
3rdparty/kdsingleapplication/README.md vendored Normal file
View File

@@ -0,0 +1,53 @@
# KDSingleApplication
`KDSingleApplication` is a helper class for single-instance policy applications
written by [KDAB](https://www.kdab.com).
## Usage
Currently the documentation is woefully lacking, but see the examples or tests
for inspiration. Basically it involves:
1. Create a `Q(Core|Gui)Application` object.
2. Create a `KDSingleApplication` object.
3. Check if the current instance is *primary* (or "master") or
*secondary* (or "slave") by calling `isPrimaryInstance`:
* the *primary* instance needs to listen from messages coming from the
secondary instances, by connecting a slot to the `messageReceived` signal;
* the *secondary* instances can send messages to the primary instance
by calling `sendMessage`.
## Licensing
KDSingleApplication is (C) 2019-2023, Klarälvdalens Datakonsult AB, and is available
under the terms of the [MIT license](LICENSES/MIT.txt).
Contact KDAB at <info@kdab.com> if you need different licensing options.
## Get Involved
KDAB will happily accept external contributions.
Please submit your contributions or issue reports from our GitHub space at
<https://github.com/KDAB/KDSingleApplication>.
## About KDAB
KDSingleApplication is supported and maintained by Klarälvdalens Datakonsult AB (KDAB).
The KDAB Group is the global No.1 software consultancy for Qt, C++ and
OpenGL applications across desktop, embedded and mobile platforms.
The KDAB Group provides consulting and mentoring for developing Qt applications
from scratch and in porting from all popular and legacy frameworks to Qt.
We continue to help develop parts of Qt and are one of the major contributors
to the Qt Project. We can give advanced or standard trainings anywhere
around the globe on Qt as well as C++, OpenGL, 3D and more.
Please visit <https://www.kdab.com> to meet the people who write code like this.
Stay up-to-date with KDAB product announcements:
* [KDAB Newsletter](https://news.kdab.com)
* [KDAB Blogs](https://www.kdab.com/category/blogs)
* [KDAB on Twitter](https://twitter.com/KDABQt)

View File

@@ -0,0 +1,108 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@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 *q);
void initialize();
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 *q)
: q_ptr(q)
, m_name(name)
, m_impl(name)
{
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, 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;

View File

@@ -0,0 +1,48 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#ifndef KDSINGLEAPPLICATION_H
#define KDSINGLEAPPLICATION_H
#include <QtCore/QObject>
#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:
explicit KDSingleApplication(QObject *parent = nullptr);
explicit KDSingleApplication(const QString &name, 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;
};
#endif // KDSINGLEAPPLICATION_H

View File

@@ -0,0 +1,23 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@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

View File

@@ -0,0 +1,306 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@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>
#endif
#if defined(Q_OS_WIN)
#include <qt_windows.h>
#endif
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, QObject *parent)
: QObject(parent)
{
#if defined(Q_OS_UNIX)
/* cppcheck-suppress useInitializationList */
m_socketName = QStringLiteral("kdsingleapp-%1-%2-%3")
.arg(::getuid())
.arg(qEnvironmentVariable("XDG_SESSION_ID"), name);
#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
DWORD sessionId;
BOOL haveSessionId = ProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
m_socketName = QString::fromUtf8("kdsingleapp-%1-%2")
.arg(haveSessionId ? sessionId : 0)
.arg(name);
#else
#error "KDSingleApplication has not been ported to this platform"
#endif
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(std::move(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);
}

View File

@@ -0,0 +1,126 @@
/*
This file is part of KDSingleApplication.
SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
SPDX-License-Identifier: MIT
Contact KDAB at <info@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>
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,
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