Replace SingleApplication with KDSingleApplication
This commit is contained in:
parent
b861703dad
commit
919ff414e6
|
@ -1,15 +1,13 @@
|
||||||
3rdparty libraries located in this directory
|
3rdparty libraries located in this directory
|
||||||
============================================
|
============================================
|
||||||
|
|
||||||
singleapplication
|
KDSingleApplication
|
||||||
-----------------
|
-----------------
|
||||||
This is a small static library used by Strawberry to prevent it from starting twice per user session.
|
This is a small static library used by Strawberry to prevent it from starting twice per user session.
|
||||||
If the user tries to start strawberry twice, the main window will maximize instead of starting another instance.
|
If the user tries to start strawberry twice, the main window will maximize instead of starting another instance.
|
||||||
If you dynamically link to your systems version, you'll need two versions, one defined as QApplication and
|
It is also used to pass command-line options through to the first instance.
|
||||||
one as a QCoreApplication.
|
|
||||||
It is included here because it is not packed by distros and is also used on macOS and Windows.
|
|
||||||
|
|
||||||
URL: https://github.com/itay-grudev/SingleApplication
|
URL: https://github.com/KDAB/KDSingleApplication/
|
||||||
|
|
||||||
|
|
||||||
SPMediaKeyTap
|
SPMediaKeyTap
|
||||||
|
|
|
@ -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} )
|
|
@ -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.
|
|
@ -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)
|
|
@ -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;
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||||
|
}
|
|
@ -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
|
|
@ -1,12 +0,0 @@
|
||||||
cmake_minimum_required(VERSION 3.7)
|
|
||||||
|
|
||||||
include(CheckIncludeFiles)
|
|
||||||
include(CheckFunctionExists)
|
|
||||||
|
|
||||||
check_function_exists(geteuid HAVE_GETEUID)
|
|
||||||
check_function_exists(getpwuid HAVE_GETPWUID)
|
|
||||||
|
|
||||||
configure_file(config.h.in "${CMAKE_CURRENT_BINARY_DIR}/config.h")
|
|
||||||
|
|
||||||
add_subdirectory(singleapplication)
|
|
||||||
add_subdirectory(singlecoreapplication)
|
|
|
@ -1,24 +0,0 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) Itay Grudev 2015 - 2020
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Note: Some of the examples include code not distributed under the terms of the
|
|
||||||
MIT License.
|
|
|
@ -1,305 +0,0 @@
|
||||||
SingleApplication
|
|
||||||
=================
|
|
||||||
[![CI](https://github.com/itay-grudev/SingleApplication/workflows/CI:%20Build%20Test/badge.svg)](https://github.com/itay-grudev/SingleApplication/actions)
|
|
||||||
|
|
||||||
This is a replacement of the QtSingleApplication for `Qt5` and `Qt6`.
|
|
||||||
|
|
||||||
Keeps the Primary Instance of your Application and kills each subsequent
|
|
||||||
instances. It can (if enabled) spawn secondary (non-related to the primary)
|
|
||||||
instances and can send data to the primary instance from secondary instances.
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
The `SingleApplication` class inherits from whatever `Q[Core|Gui]Application`
|
|
||||||
class you specify via the `QAPPLICATION_CLASS` macro (`QCoreApplication` is the
|
|
||||||
default). Further usage is similar to the use of the `Q[Core|Gui]Application`
|
|
||||||
classes.
|
|
||||||
|
|
||||||
You can use the library as if you use any other `QCoreApplication` derived
|
|
||||||
class:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
#include <QApplication>
|
|
||||||
#include <SingleApplication.h>
|
|
||||||
|
|
||||||
int main( int argc, char* argv[] )
|
|
||||||
{
|
|
||||||
SingleApplication app( argc, argv );
|
|
||||||
|
|
||||||
return app.exec();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To include the library files I would recommend that you add it as a git
|
|
||||||
submodule to your project. Here is how:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git submodule add https://github.com/itay-grudev/SingleApplication.git singleapplication
|
|
||||||
```
|
|
||||||
|
|
||||||
**Qmake:**
|
|
||||||
|
|
||||||
Then include the `singleapplication.pri` file in your `.pro` project file.
|
|
||||||
|
|
||||||
```qmake
|
|
||||||
include(singleapplication/singleapplication.pri)
|
|
||||||
DEFINES += QAPPLICATION_CLASS=QApplication
|
|
||||||
```
|
|
||||||
|
|
||||||
**CMake:**
|
|
||||||
|
|
||||||
Then include the subdirectory in your `CMakeLists.txt` project file.
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
|
|
||||||
add_subdirectory(src/third-party/singleapplication)
|
|
||||||
target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
The library sets up a `QLocalServer` and a `QSharedMemory` block. The first
|
|
||||||
instance of your Application is your Primary Instance. It would check if the
|
|
||||||
shared memory block exists and if not it will start a `QLocalServer` and listen
|
|
||||||
for connections. Each subsequent instance of your application would check if the
|
|
||||||
shared memory block exists and if it does, it will connect to the QLocalServer
|
|
||||||
to notify the primary instance that a new instance had been started, after which
|
|
||||||
it would terminate with status code `0`. In the Primary Instance
|
|
||||||
`SingleApplication` would emit the `instanceStarted()` signal upon detecting
|
|
||||||
that a new instance had been started.
|
|
||||||
|
|
||||||
The library uses `stdlib` to terminate the program with the `exit()` function.
|
|
||||||
|
|
||||||
Also don't forget to specify which `QCoreApplication` class your app is using if it
|
|
||||||
is not `QCoreApplication` as in examples above.
|
|
||||||
|
|
||||||
The `Instance Started` signal
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
The SingleApplication class implements a `instanceStarted()` signal. You can
|
|
||||||
bind to that signal to raise your application's window when a new instance had
|
|
||||||
been started, for example.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// window is a QWindow instance
|
|
||||||
QObject::connect(
|
|
||||||
&app,
|
|
||||||
&SingleApplication::instanceStarted,
|
|
||||||
&window,
|
|
||||||
&QWindow::raise
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Using `SingleApplication::instance()` is a neat way to get the
|
|
||||||
`SingleApplication` instance for binding to it's signals anywhere in your
|
|
||||||
program.
|
|
||||||
|
|
||||||
__Note:__ On Windows the ability to bring the application windows to the
|
|
||||||
foreground is restricted. See [Windows specific implementations](Windows.md)
|
|
||||||
for a workaround and an example implementation.
|
|
||||||
|
|
||||||
|
|
||||||
Secondary Instances
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
If you want to be able to launch additional Secondary Instances (not related to
|
|
||||||
your Primary Instance) you have to enable that with the third parameter of the
|
|
||||||
`SingleApplication` constructor. The default is `false` meaning no Secondary
|
|
||||||
Instances. Here is an example of how you would start a Secondary Instance send
|
|
||||||
a message with the command line arguments to the primary instance and then shut
|
|
||||||
down.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
int main(int argc, char *argv[])
|
|
||||||
{
|
|
||||||
SingleApplication app( argc, argv, true );
|
|
||||||
|
|
||||||
if( app.isSecondary() ) {
|
|
||||||
app.sendMessage( app.arguments().join(' ')).toUtf8() );
|
|
||||||
app.exit( 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.exec();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
*__Note:__ A secondary instance won't cause the emission of the
|
|
||||||
`instanceStarted()` signal by default. See `SingleApplication::Mode` for more
|
|
||||||
details.*
|
|
||||||
|
|
||||||
You can check whether your instance is a primary or secondary with the following
|
|
||||||
methods:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
app.isPrimary();
|
|
||||||
// or
|
|
||||||
app.isSecondary();
|
|
||||||
```
|
|
||||||
|
|
||||||
*__Note:__ If your Primary Instance is terminated a newly launched instance
|
|
||||||
will replace the Primary one even if the Secondary flag has been set.*
|
|
||||||
|
|
||||||
Examples
|
|
||||||
--------
|
|
||||||
|
|
||||||
There are three examples provided in this repository:
|
|
||||||
|
|
||||||
* Basic example that prevents a secondary instance from starting [`examples/basic`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/basic)
|
|
||||||
* An example of a graphical application raising it's parent window [`examples/calculator`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/calculator)
|
|
||||||
* A console application sending the primary instance it's command line parameters [`examples/sending_arguments`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/sending_arguments)
|
|
||||||
|
|
||||||
API
|
|
||||||
---
|
|
||||||
|
|
||||||
### Members
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 100, QString userData = QString() )
|
|
||||||
```
|
|
||||||
|
|
||||||
Depending on whether `allowSecondary` is set, this constructor may terminate
|
|
||||||
your app if there is already a primary instance running. Additional `Options`
|
|
||||||
can be specified to set whether the SingleApplication block should work
|
|
||||||
user-wide or system-wide. Additionally the `Mode::SecondaryNotification` may be
|
|
||||||
used to notify the primary instance whenever a secondary instance had been
|
|
||||||
started (disabled by default). `timeout` specifies the maximum time in
|
|
||||||
milliseconds to wait for blocking operations. Setting `userData` provides additional data that will isolate this instance from other instances that do not have the same (or any) user data set.
|
|
||||||
|
|
||||||
*__Note:__ `argc` and `argv` may be changed as Qt removes arguments that it
|
|
||||||
recognizes.*
|
|
||||||
|
|
||||||
*__Note:__ `Mode::SecondaryNotification` only works if set on both the primary
|
|
||||||
and the secondary instance.*
|
|
||||||
|
|
||||||
*__Note:__ Operating system can restrict the shared memory blocks to the same
|
|
||||||
user, in which case the User/System modes will have no effect and the block will
|
|
||||||
be user wide.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
bool SingleApplication::sendMessage( QByteArray message, int timeout = 100 )
|
|
||||||
```
|
|
||||||
|
|
||||||
Sends `message` to the Primary Instance. Uses `timeout` as a the maximum timeout
|
|
||||||
in milliseconds for blocking functions. Returns `true` if the message has been sent
|
|
||||||
successfully. If the message can't be sent or the function timeouts - returns `false`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
bool SingleApplication::isPrimary()
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns if the instance is the primary instance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
bool SingleApplication::isSecondary()
|
|
||||||
```
|
|
||||||
Returns if the instance is a secondary instance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
quint32 SingleApplication::instanceId()
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns a unique identifier for the current instance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
qint64 SingleApplication::primaryPid()
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns the process ID (PID) of the primary instance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
QString SingleApplication::primaryUser()
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns the username the primary instance is running as.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
QString SingleApplication::currentUser()
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns the username the current instance is running as.
|
|
||||||
|
|
||||||
### Signals
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
void SingleApplication::instanceStarted()
|
|
||||||
```
|
|
||||||
|
|
||||||
Triggered whenever a new instance had been started, except for secondary
|
|
||||||
instances if the `Mode::SecondaryNotification` flag is not specified.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
void SingleApplication::receivedMessage( quint32 instanceId, QByteArray message )
|
|
||||||
```
|
|
||||||
|
|
||||||
Triggered whenever there is a message received from a secondary instance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Flags
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
enum SingleApplication::Mode
|
|
||||||
```
|
|
||||||
|
|
||||||
* `Mode::User` - The SingleApplication block should apply user wide. This adds
|
|
||||||
user specific data to the key used for the shared memory and server name.
|
|
||||||
This is the default functionality.
|
|
||||||
* `Mode::System` – The SingleApplication block applies system-wide.
|
|
||||||
* `Mode::SecondaryNotification` – Whether to trigger `instanceStarted()` even
|
|
||||||
whenever secondary instances are started.
|
|
||||||
* `Mode::ExcludeAppPath` – Excludes the application path from the server name
|
|
||||||
(and memory block) hash.
|
|
||||||
* `Mode::ExcludeAppVersion` – Excludes the application version from the server
|
|
||||||
name (and memory block) hash.
|
|
||||||
|
|
||||||
*__Note:__ `Mode::SecondaryNotification` only works if set on both the primary
|
|
||||||
and the secondary instance.*
|
|
||||||
|
|
||||||
*__Note:__ Operating system can restrict the shared memory blocks to the same
|
|
||||||
user, in which case the User/System modes will have no effect and the block will
|
|
||||||
be user wide.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Versioning
|
|
||||||
----------
|
|
||||||
|
|
||||||
Each major version introduces either very significant changes or is not
|
|
||||||
backwards compatible with the previous version. Minor versions only add
|
|
||||||
additional features, bug fixes or performance improvements and are backwards
|
|
||||||
compatible with the previous release. See [`CHANGELOG.md`](CHANGELOG.md) for
|
|
||||||
more details.
|
|
||||||
|
|
||||||
Implementation
|
|
||||||
--------------
|
|
||||||
|
|
||||||
The library is implemented with a QSharedMemory block which is thread safe and
|
|
||||||
guarantees a race condition will not occur. It also uses a QLocalSocket to
|
|
||||||
notify the main process that a new instance had been spawned and thus invoke the
|
|
||||||
`instanceStarted()` signal and for messaging the primary instance.
|
|
||||||
|
|
||||||
Additionally the library can recover from being forcefully killed on *nix
|
|
||||||
systems and will reset the memory block given that there are no other
|
|
||||||
instances running.
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
This library and it's supporting documentation are released under
|
|
||||||
`The MIT License (MIT)` with the exception of the Qt calculator examples which
|
|
||||||
is distributed under the BSD license.
|
|
|
@ -1,2 +0,0 @@
|
||||||
#cmakedefine HAVE_GETEUID
|
|
||||||
#cmakedefine HAVE_GETPWUID
|
|
|
@ -1,18 +0,0 @@
|
||||||
cmake_minimum_required(VERSION 3.7)
|
|
||||||
|
|
||||||
add_definitions(-DSINGLEAPPLICATION)
|
|
||||||
|
|
||||||
set(SOURCES ../singleapplication_t.cpp ../singleapplication_p.cpp)
|
|
||||||
set(HEADERS ../singleapplication_t.h ../singleapplication_p.h)
|
|
||||||
qt_wrap_cpp(MOC ${HEADERS})
|
|
||||||
add_library(singleapplication STATIC ${SOURCES} ${MOC})
|
|
||||||
target_include_directories(singleapplication PUBLIC
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/..
|
|
||||||
${Boost_INCLUDE_DIRS}
|
|
||||||
)
|
|
||||||
target_link_libraries(singleapplication PUBLIC
|
|
||||||
${QtCore_LIBRARIES}
|
|
||||||
${QtWidgets_LIBRARIES}
|
|
||||||
${QtNetwork_LIBRARIES}
|
|
||||||
)
|
|
|
@ -1,13 +0,0 @@
|
||||||
#ifndef SINGLEAPPLICATION_H
|
|
||||||
#define SINGLEAPPLICATION_H
|
|
||||||
|
|
||||||
#ifdef SINGLEAPPLICATION
|
|
||||||
# error "SINGLEAPPLICATION already defined."
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define SINGLEAPPLICATION
|
|
||||||
#include "../singleapplication_t.h"
|
|
||||||
#undef SINGLEAPPLICATION_T_H
|
|
||||||
#undef SINGLEAPPLICATION
|
|
||||||
|
|
||||||
#endif // SINGLEAPPLICATION_H
|
|
|
@ -1,505 +0,0 @@
|
||||||
// The MIT License (MIT)
|
|
||||||
//
|
|
||||||
// Copyright (c) Itay Grudev 2015 - 2020
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
//
|
|
||||||
// W A R N I N G !!!
|
|
||||||
// -----------------
|
|
||||||
//
|
|
||||||
// This is a modified version of SingleApplication,
|
|
||||||
// The original version is at:
|
|
||||||
//
|
|
||||||
// https://github.com/itay-grudev/SingleApplication
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstddef>
|
|
||||||
|
|
||||||
#ifdef Q_OS_UNIX
|
|
||||||
# include <unistd.h>
|
|
||||||
# include <sys/types.h>
|
|
||||||
# include <pwd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
|
||||||
# ifndef NOMINMAX
|
|
||||||
# define NOMINMAX 1
|
|
||||||
# endif
|
|
||||||
# include <windows.h>
|
|
||||||
# include <lmcons.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QThread>
|
|
||||||
#include <QIODevice>
|
|
||||||
#include <QSharedMemory>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QDataStream>
|
|
||||||
#include <QCryptographicHash>
|
|
||||||
#include <QLocalServer>
|
|
||||||
#include <QLocalSocket>
|
|
||||||
#include <QElapsedTimer>
|
|
||||||
#include <QRandomGenerator>
|
|
||||||
|
|
||||||
#include "singleapplication_t.h"
|
|
||||||
#include "singleapplication_p.h"
|
|
||||||
|
|
||||||
SingleApplicationPrivateClass::SingleApplicationPrivateClass(SingleApplicationClass *ptr)
|
|
||||||
: q_ptr(ptr),
|
|
||||||
memory_(nullptr),
|
|
||||||
socket_(nullptr),
|
|
||||||
server_(nullptr),
|
|
||||||
instanceNumber_(-1) {}
|
|
||||||
|
|
||||||
SingleApplicationPrivateClass::~SingleApplicationPrivateClass() {
|
|
||||||
|
|
||||||
if (socket_ != nullptr && socket_->isOpen()) {
|
|
||||||
socket_->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (memory_ != nullptr) {
|
|
||||||
memory_->lock();
|
|
||||||
if (server_ != nullptr) {
|
|
||||||
server_->close();
|
|
||||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
|
||||||
instance->primary = false;
|
|
||||||
instance->primaryPid = -1;
|
|
||||||
instance->primaryUser[0] = '\0';
|
|
||||||
instance->checksum = blockChecksum();
|
|
||||||
}
|
|
||||||
memory_->unlock();
|
|
||||||
if (memory_->isAttached()) {
|
|
||||||
memory_->detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SingleApplicationPrivateClass::getUsername() {
|
|
||||||
|
|
||||||
#ifdef Q_OS_UNIX
|
|
||||||
QString username;
|
|
||||||
#if defined(HAVE_GETEUID) && defined(HAVE_GETPWUID)
|
|
||||||
struct passwd *pw = getpwuid(geteuid());
|
|
||||||
if (pw) {
|
|
||||||
username = QString::fromLocal8Bit(pw->pw_name);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (username.isEmpty()) {
|
|
||||||
username = qEnvironmentVariable("USER");
|
|
||||||
}
|
|
||||||
return username;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
|
||||||
wchar_t username[UNLEN + 1];
|
|
||||||
// Specifies size of the buffer on input
|
|
||||||
DWORD usernameLength = UNLEN + 1;
|
|
||||||
if (GetUserNameW(username, &usernameLength)) {
|
|
||||||
return QString::fromWCharArray(username);
|
|
||||||
}
|
|
||||||
return qEnvironmentVariable("USERNAME");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::genBlockServerName() {
|
|
||||||
|
|
||||||
QCryptographicHash appData(QCryptographicHash::Sha256);
|
|
||||||
appData.addData("SingleApplication");
|
|
||||||
appData.addData(SingleApplicationClass::applicationName().toUtf8());
|
|
||||||
appData.addData(SingleApplicationClass::organizationName().toUtf8());
|
|
||||||
appData.addData(SingleApplicationClass::organizationDomain().toUtf8());
|
|
||||||
|
|
||||||
if (!(options_ & SingleApplicationClass::Mode::ExcludeAppVersion)) {
|
|
||||||
appData.addData(SingleApplicationClass::applicationVersion().toUtf8());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(options_ & SingleApplicationClass::Mode::ExcludeAppPath)) {
|
|
||||||
#if defined(Q_OS_UNIX)
|
|
||||||
const QByteArray appImagePath = qgetenv("APPIMAGE");
|
|
||||||
if (appImagePath.isEmpty()) {
|
|
||||||
appData.addData(SingleApplicationClass::applicationFilePath().toUtf8());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
appData.addData(appImagePath);
|
|
||||||
}
|
|
||||||
#elif defined(Q_OS_WIN)
|
|
||||||
appData.addData(SingleApplicationClass::applicationFilePath().toLower().toUtf8());
|
|
||||||
#else
|
|
||||||
appData.addData(SingleApplicationClass::applicationFilePath().toUtf8());
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// User level block requires a user specific data in the hash
|
|
||||||
if (options_ & SingleApplicationClass::Mode::User) {
|
|
||||||
appData.addData(getUsername().toUtf8());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with server naming requirements.
|
|
||||||
blockServerName_ = appData.result().toBase64().replace("/", "_");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::initializeMemoryBlock() const {
|
|
||||||
|
|
||||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
|
||||||
instance->primary = false;
|
|
||||||
instance->secondary = 0;
|
|
||||||
instance->primaryPid = -1;
|
|
||||||
instance->primaryUser[0] = '\0';
|
|
||||||
instance->checksum = blockChecksum();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::startPrimary() {
|
|
||||||
|
|
||||||
// Reset the number of connections
|
|
||||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
|
||||||
|
|
||||||
instance->primary = true;
|
|
||||||
instance->primaryPid = QCoreApplication::applicationPid();
|
|
||||||
qstrncpy(instance->primaryUser, getUsername().toUtf8().data(), sizeof(instance->primaryUser));
|
|
||||||
instance->checksum = blockChecksum();
|
|
||||||
instanceNumber_ = 0;
|
|
||||||
// Successful creation means that no main process exists
|
|
||||||
// So we start a QLocalServer to listen for connections
|
|
||||||
QLocalServer::removeServer(blockServerName_);
|
|
||||||
server_ = new QLocalServer(this);
|
|
||||||
|
|
||||||
// Restrict access to the socket according to the SingleApplication::Mode::User flag on User level or no restrictions
|
|
||||||
if (options_ & SingleApplicationClass::Mode::User) {
|
|
||||||
server_->setSocketOptions(QLocalServer::UserAccessOption);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
server_->setSocketOptions(QLocalServer::WorldAccessOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
server_->listen(blockServerName_);
|
|
||||||
QObject::connect(server_, &QLocalServer::newConnection, this, &SingleApplicationPrivateClass::slotConnectionEstablished);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::startSecondary() {
|
|
||||||
|
|
||||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
|
||||||
|
|
||||||
instance->secondary += 1;
|
|
||||||
instance->checksum = blockChecksum();
|
|
||||||
instanceNumber_ = instance->secondary;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SingleApplicationPrivateClass::connectToPrimary(const int timeout, const ConnectionType connectionType) {
|
|
||||||
|
|
||||||
// Connect to the Local Server of the Primary Instance if not already connected.
|
|
||||||
if (socket_ == nullptr) {
|
|
||||||
socket_ = new QLocalSocket(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socket_->state() == QLocalSocket::ConnectedState) return true;
|
|
||||||
|
|
||||||
QElapsedTimer time;
|
|
||||||
time.start();
|
|
||||||
|
|
||||||
if (socket_->state() != QLocalSocket::ConnectedState) {
|
|
||||||
|
|
||||||
forever {
|
|
||||||
randomSleep();
|
|
||||||
|
|
||||||
if (socket_->state() != QLocalSocket::ConnectingState) {
|
|
||||||
socket_->connectToServer(blockServerName_);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socket_->state() == QLocalSocket::ConnectingState) {
|
|
||||||
socket_->waitForConnected(static_cast<int>(timeout - time.elapsed()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If connected break out of the loop
|
|
||||||
if (socket_->state() == QLocalSocket::ConnectedState) break;
|
|
||||||
|
|
||||||
// If elapsed time since start is longer than the method timeout return
|
|
||||||
if (time.elapsed() >= timeout) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialization message according to the SingleApplication protocol
|
|
||||||
QByteArray initMsg;
|
|
||||||
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
|
|
||||||
writeStream.setVersion(QDataStream::Qt_5_8);
|
|
||||||
|
|
||||||
writeStream << blockServerName_.toLatin1();
|
|
||||||
writeStream << static_cast<quint8>(connectionType);
|
|
||||||
writeStream << instanceNumber_;
|
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
||||||
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
|
|
||||||
#else
|
|
||||||
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
writeStream << checksum;
|
|
||||||
|
|
||||||
return writeConfirmedMessage(static_cast<int>(timeout - time.elapsed()), initMsg);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::writeAck(QLocalSocket *sock) {
|
|
||||||
sock->putChar('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SingleApplicationPrivateClass::writeConfirmedMessage(const int timeout, const QByteArray &msg) const {
|
|
||||||
|
|
||||||
QElapsedTimer time;
|
|
||||||
time.start();
|
|
||||||
|
|
||||||
// Frame 1: The header indicates the message length that follows
|
|
||||||
QByteArray header;
|
|
||||||
QDataStream headerStream(&header, QIODevice::WriteOnly);
|
|
||||||
headerStream.setVersion(QDataStream::Qt_5_8);
|
|
||||||
headerStream << static_cast<quint64>(msg.length());
|
|
||||||
|
|
||||||
if (!writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), header)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frame 2: The message
|
|
||||||
return writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), msg);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SingleApplicationPrivateClass::writeConfirmedFrame(const int timeout, const QByteArray &msg) const {
|
|
||||||
|
|
||||||
socket_->write(msg);
|
|
||||||
socket_->flush();
|
|
||||||
|
|
||||||
bool result = socket_->waitForReadyRead(timeout);
|
|
||||||
if (result) {
|
|
||||||
socket_->read(1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
quint16 SingleApplicationPrivateClass::blockChecksum() const {
|
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
||||||
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum)));
|
|
||||||
#else
|
|
||||||
quint16 checksum = qChecksum(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return checksum;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
qint64 SingleApplicationPrivateClass::primaryPid() const {
|
|
||||||
|
|
||||||
memory_->lock();
|
|
||||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
|
||||||
qint64 pid = instance->primaryPid;
|
|
||||||
memory_->unlock();
|
|
||||||
|
|
||||||
return pid;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SingleApplicationPrivateClass::primaryUser() const {
|
|
||||||
|
|
||||||
memory_->lock();
|
|
||||||
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
|
||||||
QByteArray username = instance->primaryUser;
|
|
||||||
memory_->unlock();
|
|
||||||
|
|
||||||
return QString::fromUtf8(username);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Executed when a connection has been made to the LocalServer
|
|
||||||
*/
|
|
||||||
void SingleApplicationPrivateClass::slotConnectionEstablished() {
|
|
||||||
|
|
||||||
QLocalSocket *nextConnSocket = server_->nextPendingConnection();
|
|
||||||
connectionMap_.insert(nextConnSocket, ConnectionInfo());
|
|
||||||
|
|
||||||
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [this, nextConnSocket]() {
|
|
||||||
const ConnectionInfo &info = connectionMap_[nextConnSocket];
|
|
||||||
slotClientConnectionClosed(nextConnSocket, info.instanceId);
|
|
||||||
});
|
|
||||||
|
|
||||||
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
|
|
||||||
|
|
||||||
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [this, nextConnSocket]() {
|
|
||||||
connectionMap_.remove(nextConnSocket);
|
|
||||||
});
|
|
||||||
|
|
||||||
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [this, nextConnSocket]() {
|
|
||||||
const ConnectionInfo &info = connectionMap_[nextConnSocket];
|
|
||||||
switch (info.stage) {
|
|
||||||
case StageInitHeader:
|
|
||||||
readMessageHeader(nextConnSocket, StageInitBody);
|
|
||||||
break;
|
|
||||||
case StageInitBody:
|
|
||||||
readInitMessageBody(nextConnSocket);
|
|
||||||
break;
|
|
||||||
case StageConnectedHeader:
|
|
||||||
readMessageHeader(nextConnSocket, StageConnectedBody);
|
|
||||||
break;
|
|
||||||
case StageConnectedBody:
|
|
||||||
slotDataAvailable(nextConnSocket, info.instanceId);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::readMessageHeader(QLocalSocket *sock, const SingleApplicationPrivateClass::ConnectionStage nextStage) {
|
|
||||||
|
|
||||||
if (!connectionMap_.contains(sock)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sock->bytesAvailable() < static_cast<qint64>(sizeof(quint64))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDataStream headerStream(sock);
|
|
||||||
headerStream.setVersion(QDataStream::Qt_5_8);
|
|
||||||
|
|
||||||
// Read the header to know the message length
|
|
||||||
quint64 msgLen = 0;
|
|
||||||
headerStream >> msgLen;
|
|
||||||
ConnectionInfo &info = connectionMap_[sock];
|
|
||||||
info.stage = nextStage;
|
|
||||||
info.msgLen = msgLen;
|
|
||||||
|
|
||||||
writeAck(sock);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SingleApplicationPrivateClass::isFrameComplete(QLocalSocket *sock) {
|
|
||||||
|
|
||||||
if (!connectionMap_.contains(sock)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConnectionInfo &info = connectionMap_[sock];
|
|
||||||
return (sock->bytesAvailable() >= static_cast<qint64>(info.msgLen));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::readInitMessageBody(QLocalSocket *sock) {
|
|
||||||
|
|
||||||
Q_Q(SingleApplicationClass);
|
|
||||||
|
|
||||||
if (!isFrameComplete(sock)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the message body
|
|
||||||
QByteArray msgBytes = sock->readAll();
|
|
||||||
QDataStream readStream(msgBytes);
|
|
||||||
readStream.setVersion(QDataStream::Qt_5_8);
|
|
||||||
|
|
||||||
// server name
|
|
||||||
QByteArray latin1Name;
|
|
||||||
readStream >> latin1Name;
|
|
||||||
|
|
||||||
// connection type
|
|
||||||
quint8 connTypeVal = InvalidConnection;
|
|
||||||
readStream >> connTypeVal;
|
|
||||||
const ConnectionType connectionType = static_cast<ConnectionType>(connTypeVal);
|
|
||||||
|
|
||||||
// instance id
|
|
||||||
quint32 instanceId = 0;
|
|
||||||
readStream >> instanceId;
|
|
||||||
|
|
||||||
// checksum
|
|
||||||
quint16 msgChecksum = 0;
|
|
||||||
readStream >> msgChecksum;
|
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
||||||
const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast<quint32>(msgBytes.length() - sizeof(quint16))));
|
|
||||||
#else
|
|
||||||
const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16)));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName_ && msgChecksum == actualChecksum;
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
sock->close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionInfo &info = connectionMap_[sock];
|
|
||||||
info.instanceId = instanceId;
|
|
||||||
info.stage = StageConnectedHeader;
|
|
||||||
|
|
||||||
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplicationClass::Mode::SecondaryNotification)) {
|
|
||||||
emit q->instanceStarted();
|
|
||||||
}
|
|
||||||
|
|
||||||
writeAck(sock);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
|
|
||||||
|
|
||||||
Q_Q(SingleApplicationClass);
|
|
||||||
|
|
||||||
if (!isFrameComplete(dataSocket)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QByteArray message = dataSocket->readAll();
|
|
||||||
|
|
||||||
writeAck(dataSocket);
|
|
||||||
|
|
||||||
ConnectionInfo &info = connectionMap_[dataSocket];
|
|
||||||
info.stage = StageConnectedHeader;
|
|
||||||
|
|
||||||
emit q->receivedMessage(instanceId, message);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) {
|
|
||||||
|
|
||||||
if (closedSocket->bytesAvailable() > 0) {
|
|
||||||
slotDataAvailable(closedSocket, instanceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void SingleApplicationPrivateClass::randomSleep() {
|
|
||||||
|
|
||||||
QThread::msleep(QRandomGenerator::global()->bounded(8U, 18U));
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
// The MIT License (MIT)
|
|
||||||
//
|
|
||||||
// Copyright (c) Itay Grudev 2015 - 2020
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
//
|
|
||||||
// W A R N I N G !!!
|
|
||||||
// -----------------
|
|
||||||
//
|
|
||||||
// This is a modified version of SingleApplication,
|
|
||||||
// The original version is at:
|
|
||||||
//
|
|
||||||
// https://github.com/itay-grudev/SingleApplication
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef SINGLEAPPLICATION_P_H
|
|
||||||
#define SINGLEAPPLICATION_P_H
|
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QHash>
|
|
||||||
|
|
||||||
#include "singleapplication_t.h"
|
|
||||||
|
|
||||||
class QLocalServer;
|
|
||||||
class QLocalSocket;
|
|
||||||
class QSharedMemory;
|
|
||||||
|
|
||||||
class SingleApplicationPrivateClass : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit SingleApplicationPrivateClass(SingleApplicationClass *ptr);
|
|
||||||
~SingleApplicationPrivateClass() override;
|
|
||||||
|
|
||||||
enum ConnectionType : quint8 {
|
|
||||||
InvalidConnection = 0,
|
|
||||||
NewInstance = 1,
|
|
||||||
SecondaryInstance = 2,
|
|
||||||
Reconnect = 3
|
|
||||||
};
|
|
||||||
enum ConnectionStage : quint8 {
|
|
||||||
StageInitHeader = 0,
|
|
||||||
StageInitBody = 1,
|
|
||||||
StageConnectedHeader = 2,
|
|
||||||
StageConnectedBody = 3
|
|
||||||
};
|
|
||||||
Q_DECLARE_PUBLIC(SingleApplicationClass)
|
|
||||||
|
|
||||||
struct InstancesInfo {
|
|
||||||
explicit InstancesInfo() : primary(false), secondary(0), primaryPid(0), checksum(0) {}
|
|
||||||
bool primary;
|
|
||||||
quint32 secondary;
|
|
||||||
qint64 primaryPid;
|
|
||||||
char primaryUser[128];
|
|
||||||
quint16 checksum;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ConnectionInfo {
|
|
||||||
explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {}
|
|
||||||
quint64 msgLen;
|
|
||||||
quint32 instanceId;
|
|
||||||
quint8 stage;
|
|
||||||
};
|
|
||||||
|
|
||||||
static QString getUsername();
|
|
||||||
void genBlockServerName();
|
|
||||||
void initializeMemoryBlock() const;
|
|
||||||
void startPrimary();
|
|
||||||
void startSecondary();
|
|
||||||
bool connectToPrimary(const int timeout, const ConnectionType connectionType);
|
|
||||||
quint16 blockChecksum() const;
|
|
||||||
qint64 primaryPid() const;
|
|
||||||
QString primaryUser() const;
|
|
||||||
bool isFrameComplete(QLocalSocket *sock);
|
|
||||||
void readMessageHeader(QLocalSocket *socket, const ConnectionStage nextStage);
|
|
||||||
void readInitMessageBody(QLocalSocket *socket);
|
|
||||||
void writeAck(QLocalSocket *sock);
|
|
||||||
bool writeConfirmedFrame(const int timeout, const QByteArray &msg) const;
|
|
||||||
bool writeConfirmedMessage(const int timeout, const QByteArray &msg) const;
|
|
||||||
static void randomSleep();
|
|
||||||
|
|
||||||
SingleApplicationClass *q_ptr;
|
|
||||||
QSharedMemory *memory_;
|
|
||||||
QLocalSocket *socket_;
|
|
||||||
QLocalServer *server_;
|
|
||||||
quint32 instanceNumber_;
|
|
||||||
QString blockServerName_;
|
|
||||||
SingleApplicationClass::Options options_;
|
|
||||||
QHash<QLocalSocket*, ConnectionInfo> connectionMap_;
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void slotConnectionEstablished();
|
|
||||||
void slotDataAvailable(QLocalSocket*, const quint32);
|
|
||||||
void slotClientConnectionClosed(QLocalSocket*, const quint32);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // SINGLEAPPLICATION_P_H
|
|
|
@ -1,330 +0,0 @@
|
||||||
// The MIT License (MIT)
|
|
||||||
//
|
|
||||||
// Copyright (c) Itay Grudev 2015 - 2020
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
//
|
|
||||||
// W A R N I N G !!!
|
|
||||||
// -----------------
|
|
||||||
//
|
|
||||||
// This is a modified version of SingleApplication,
|
|
||||||
// The original version is at:
|
|
||||||
//
|
|
||||||
// https://github.com/itay-grudev/SingleApplication
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <limits>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include <boost/scope_exit.hpp>
|
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
#include <QThread>
|
|
||||||
#include <QSharedMemory>
|
|
||||||
#include <QLocalSocket>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QElapsedTimer>
|
|
||||||
#include <QtDebug>
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
|
||||||
# include <QNativeIpcKey>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "singleapplication_t.h"
|
|
||||||
#include "singleapplication_p.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Constructor. Checks and fires up LocalServer or closes the program if another instance already exists
|
|
||||||
* @param argc
|
|
||||||
* @param argv
|
|
||||||
* @param allowSecondary Whether to enable secondary instance support
|
|
||||||
* @param options Optional flags to toggle specific behaviour
|
|
||||||
* @param timeout Maximum time blocking functions are allowed during app load
|
|
||||||
*/
|
|
||||||
SingleApplicationClass::SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
|
|
||||||
: ApplicationClass(argc, argv),
|
|
||||||
d_ptr(new SingleApplicationPrivateClass(this)) {
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_D(SingleCoreApplication);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Store the current mode of the program
|
|
||||||
d->options_ = options;
|
|
||||||
|
|
||||||
// Generating an application ID used for identifying the shared memory block and QLocalServer
|
|
||||||
d->genBlockServerName();
|
|
||||||
|
|
||||||
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
|
|
||||||
SingleApplicationPrivateClass::randomSleep();
|
|
||||||
|
|
||||||
#ifdef Q_OS_UNIX
|
|
||||||
// By explicitly attaching it and then deleting it we make sure that the memory is deleted even after the process has crashed on Unix.
|
|
||||||
{
|
|
||||||
# if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
|
||||||
std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(QNativeIpcKey(d->blockServerName_));
|
|
||||||
# else
|
|
||||||
std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(d->blockServerName_);
|
|
||||||
# endif
|
|
||||||
if (memory->attach()) {
|
|
||||||
memory->detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Guarantee thread safe behaviour with a shared memory block.
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
|
||||||
QSharedMemory *memory = new QSharedMemory(QNativeIpcKey(d->blockServerName_), this);
|
|
||||||
#else
|
|
||||||
QSharedMemory *memory = new QSharedMemory(d->blockServerName_, this);
|
|
||||||
#endif
|
|
||||||
d->memory_ = memory;
|
|
||||||
|
|
||||||
bool primary = false;
|
|
||||||
|
|
||||||
// Create a shared memory block
|
|
||||||
if (d->memory_->create(sizeof(SingleApplicationPrivateClass::InstancesInfo))) {
|
|
||||||
primary = true;
|
|
||||||
}
|
|
||||||
else if (d->memory_->error() == QSharedMemory::AlreadyExists) {
|
|
||||||
if (!d->memory_->attach()) {
|
|
||||||
qCritical() << "SingleApplication: Unable to attach to shared memory block:" << d->memory_->error() << d->memory_->errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qCritical() << "SingleApplication: Unable to create shared memory block:" << d->memory_->error() << d->memory_->errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool locked = false;
|
|
||||||
|
|
||||||
BOOST_SCOPE_EXIT((memory)(&locked)) {
|
|
||||||
if (locked && !memory->unlock()) {
|
|
||||||
qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}BOOST_SCOPE_EXIT_END
|
|
||||||
|
|
||||||
if (!d->memory_->lock()) {
|
|
||||||
qCritical() << "SingleApplication: Unable to lock shared memory block:" << d->memory_->error() << d->memory_->errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
locked = true;
|
|
||||||
|
|
||||||
if (primary) {
|
|
||||||
// Initialize the shared memory block
|
|
||||||
d->initializeMemoryBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
SingleApplicationPrivateClass::InstancesInfo *instance = static_cast<SingleApplicationPrivateClass::InstancesInfo*>(d->memory_->data());
|
|
||||||
QElapsedTimer time;
|
|
||||||
time.start();
|
|
||||||
|
|
||||||
// Make sure the shared memory block is initialized and in a consistent state
|
|
||||||
while (d->blockChecksum() != instance->checksum) {
|
|
||||||
|
|
||||||
// If more than 5 seconds have elapsed, assume the primary instance crashed and assume its position
|
|
||||||
if (time.elapsed() > 5000) {
|
|
||||||
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5 seconds. Assuming primary instance failure.";
|
|
||||||
d->initializeMemoryBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise wait for a random period and try again.
|
|
||||||
// The random sleep here limits the probability of a collision between two racing apps and allows the app to initialize faster
|
|
||||||
if (locked) {
|
|
||||||
if (d->memory_->unlock()) {
|
|
||||||
locked = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qCritical() << "SingleApplication: Unable to unlock shared memory block for random wait:" << memory->error() << memory->errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SingleApplicationPrivateClass::randomSleep();
|
|
||||||
|
|
||||||
if (!d->memory_->lock()) {
|
|
||||||
qCritical() << "SingleApplication: Unable to lock shared memory block after random wait:" << memory->error() << memory->errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
locked = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance->primary) {
|
|
||||||
// Check if another instance can be started
|
|
||||||
if (allowSecondary) {
|
|
||||||
d->startSecondary();
|
|
||||||
if (d->options_ & Mode::SecondaryNotification) {
|
|
||||||
d->connectToPrimary(timeout, SingleApplicationPrivateClass::SecondaryInstance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
d->startPrimary();
|
|
||||||
primary = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locked) {
|
|
||||||
if (d->memory_->unlock()) {
|
|
||||||
locked = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!primary && !allowSecondary) {
|
|
||||||
d->connectToPrimary(timeout, SingleApplicationPrivateClass::NewInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
SingleApplicationClass::~SingleApplicationClass() {
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_D(SingleCoreApplication);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
delete d;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the current application instance is primary.
|
|
||||||
* @return Returns true if the instance is primary, false otherwise.
|
|
||||||
*/
|
|
||||||
bool SingleApplicationClass::isPrimary() const {
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_D(const SingleApplication);
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_D(const SingleCoreApplication);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return d->server_ != nullptr;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the current application instance is secondary.
|
|
||||||
* @return Returns true if the instance is secondary, false otherwise.
|
|
||||||
*/
|
|
||||||
bool SingleApplicationClass::isSecondary() const {
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_D(const SingleApplication);
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_D(const SingleCoreApplication);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return d->server_ == nullptr;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows you to identify an instance by returning unique consecutive instance ids.
|
|
||||||
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
|
|
||||||
* @return Returns a unique instance id.
|
|
||||||
*/
|
|
||||||
quint32 SingleApplicationClass::instanceId() const {
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_D(const SingleApplication);
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_D(const SingleCoreApplication);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return d->instanceNumber_;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the OS PID (Process Identifier) of the process running the primary instance.
|
|
||||||
* Especially useful when SingleApplication is coupled with OS. specific APIs.
|
|
||||||
* @return Returns the primary instance PID.
|
|
||||||
*/
|
|
||||||
qint64 SingleApplicationClass::primaryPid() const {
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_D(const SingleApplication);
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_D(const SingleCoreApplication);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return d->primaryPid();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the username the primary instance is running as.
|
|
||||||
* @return Returns the username the primary instance is running as.
|
|
||||||
*/
|
|
||||||
QString SingleApplicationClass::primaryUser() const {
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_D(const SingleApplication);
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_D(const SingleCoreApplication);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return d->primaryUser();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the username the current instance is running as.
|
|
||||||
* @return Returns the username the current instance is running as.
|
|
||||||
*/
|
|
||||||
QString SingleApplicationClass::currentUser() const {
|
|
||||||
return SingleApplicationPrivateClass::getUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends message to the Primary Instance.
|
|
||||||
* @param message The message to send.
|
|
||||||
* @param timeout the maximum timeout in milliseconds for blocking functions.
|
|
||||||
* @return true if the message was sent successfully, false otherwise.
|
|
||||||
*/
|
|
||||||
bool SingleApplicationClass::sendMessage(const QByteArray &message, const int timeout) {
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_D(SingleCoreApplication);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Nobody to connect to
|
|
||||||
if (isPrimary()) return false;
|
|
||||||
|
|
||||||
// Make sure the socket is connected
|
|
||||||
if (!d->connectToPrimary(timeout, SingleApplicationPrivateClass::Reconnect)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return d->writeConfirmedMessage(timeout, message);
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,172 +0,0 @@
|
||||||
// The MIT License (MIT)
|
|
||||||
//
|
|
||||||
// Copyright (c) Itay Grudev 2015 - 2020
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
//
|
|
||||||
// W A R N I N G !!!
|
|
||||||
// -----------------
|
|
||||||
//
|
|
||||||
// This is a modified version of SingleApplication,
|
|
||||||
// The original version is at:
|
|
||||||
//
|
|
||||||
// https://github.com/itay-grudev/SingleApplication
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
#ifndef SINGLEAPPLICATION_T_H
|
|
||||||
#define SINGLEAPPLICATION_T_H
|
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
|
|
||||||
#undef ApplicationClass
|
|
||||||
#undef SingleApplicationClass
|
|
||||||
#undef SingleApplicationPrivateClass
|
|
||||||
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
# include <QApplication>
|
|
||||||
# define ApplicationClass QApplication
|
|
||||||
# define SingleApplicationClass SingleApplication
|
|
||||||
# define SingleApplicationPrivateClass SingleApplicationPrivate
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
# include <QCoreApplication>
|
|
||||||
# define ApplicationClass QCoreApplication
|
|
||||||
# define SingleApplicationClass SingleCoreApplication
|
|
||||||
# define SingleApplicationPrivateClass SingleCoreApplicationPrivate
|
|
||||||
#else
|
|
||||||
# error "Define SINGLEAPPLICATION or SINGLECOREAPPLICATION."
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <QFlags>
|
|
||||||
#include <QByteArray>
|
|
||||||
|
|
||||||
class SingleApplicationPrivateClass;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The SingleApplication class handles multiple instances of the same Application
|
|
||||||
* @see QApplication
|
|
||||||
*/
|
|
||||||
class SingleApplicationClass : public ApplicationClass { // clazy:exclude=ctor-missing-parent-argument
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* @brief Mode of operation of SingleApplication.
|
|
||||||
* Whether the block should be user-wide or system-wide and whether the
|
|
||||||
* primary instance should be notified when a secondary instance had been
|
|
||||||
* started.
|
|
||||||
* @note Operating system can restrict the shared memory blocks to the same
|
|
||||||
* user, in which case the User/System modes will have no effect and the
|
|
||||||
* block will be user wide.
|
|
||||||
* @enum
|
|
||||||
*/
|
|
||||||
enum class Mode {
|
|
||||||
User = 1 << 0,
|
|
||||||
System = 1 << 1,
|
|
||||||
SecondaryNotification = 1 << 2,
|
|
||||||
ExcludeAppVersion = 1 << 3,
|
|
||||||
ExcludeAppPath = 1 << 4
|
|
||||||
};
|
|
||||||
Q_DECLARE_FLAGS(Options, Mode)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Intitializes a SingleApplication instance with argc command line
|
|
||||||
* arguments in argv
|
|
||||||
* @arg {int &} argc - Number of arguments in argv
|
|
||||||
* @arg {const char *[]} argv - Supplied command line arguments
|
|
||||||
* @arg {bool} allowSecondary - Whether to start the instance as secondary
|
|
||||||
* if there is already a primary instance.
|
|
||||||
* @arg {Mode} mode - Whether for the SingleApplication block to be applied
|
|
||||||
* User wide or System wide.
|
|
||||||
* @arg {int} timeout - Timeout to wait in milliseconds.
|
|
||||||
* @note argc and argv may be changed as Qt removes arguments that it
|
|
||||||
* recognizes
|
|
||||||
* @note Mode::SecondaryNotification only works if set on both the primary
|
|
||||||
* instance and the secondary instance.
|
|
||||||
* @note The timeout is just a hint for the maximum time of blocking
|
|
||||||
* operations. It does not guarantee that the SingleApplication
|
|
||||||
* initialization will be completed in given time, though is a good hint.
|
|
||||||
* Usually 4*timeout would be the worst case (fail) scenario.
|
|
||||||
*/
|
|
||||||
explicit SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
|
|
||||||
~SingleApplicationClass() override;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns if the instance is the primary instance
|
|
||||||
* @returns {bool}
|
|
||||||
*/
|
|
||||||
bool isPrimary() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns if the instance is a secondary instance
|
|
||||||
* @returns {bool}
|
|
||||||
*/
|
|
||||||
bool isSecondary() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns a unique identifier for the current instance
|
|
||||||
* @returns {qint32}
|
|
||||||
*/
|
|
||||||
quint32 instanceId() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the process ID (PID) of the primary instance
|
|
||||||
* @returns {qint64}
|
|
||||||
*/
|
|
||||||
qint64 primaryPid() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the username of the user running the primary instance
|
|
||||||
* @returns {QString}
|
|
||||||
*/
|
|
||||||
QString primaryUser() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Returns the username of the current user
|
|
||||||
* @returns {QString}
|
|
||||||
*/
|
|
||||||
QString currentUser() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Sends a message to the primary instance. Returns true on success.
|
|
||||||
* @param {int} timeout - Timeout for connecting
|
|
||||||
* @returns {bool}
|
|
||||||
* @note sendMessage() will return false if invoked from the primary
|
|
||||||
* instance.
|
|
||||||
*/
|
|
||||||
bool sendMessage(const QByteArray &message, const int timeout = 1000);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void instanceStarted();
|
|
||||||
void receivedMessage(quint32 instanceId, QByteArray message);
|
|
||||||
|
|
||||||
private:
|
|
||||||
SingleApplicationPrivateClass *d_ptr;
|
|
||||||
#if defined(SINGLEAPPLICATION)
|
|
||||||
Q_DECLARE_PRIVATE(SingleApplication)
|
|
||||||
#elif defined(SINGLECOREAPPLICATION)
|
|
||||||
Q_DECLARE_PRIVATE(SingleCoreApplication)
|
|
||||||
#endif
|
|
||||||
void abortSafely();
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplicationClass::Options)
|
|
||||||
|
|
||||||
#endif // SINGLEAPPLICATION_T_H
|
|
|
@ -1,17 +0,0 @@
|
||||||
cmake_minimum_required(VERSION 3.7)
|
|
||||||
|
|
||||||
add_definitions(-DSINGLECOREAPPLICATION)
|
|
||||||
|
|
||||||
set(SOURCES ../singleapplication_t.cpp ../singleapplication_p.cpp)
|
|
||||||
set(HEADERS ../singleapplication_t.h ../singleapplication_p.h)
|
|
||||||
qt_wrap_cpp(MOC ${HEADERS})
|
|
||||||
add_library(singlecoreapplication STATIC ${SOURCES} ${MOC})
|
|
||||||
target_include_directories(singlecoreapplication PUBLIC
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/..
|
|
||||||
${Boost_INCLUDE_DIRS}
|
|
||||||
)
|
|
||||||
target_link_libraries(singlecoreapplication PUBLIC
|
|
||||||
${QtCore_LIBRARIES}
|
|
||||||
${QtNetwork_LIBRARIES}
|
|
||||||
)
|
|
|
@ -1,13 +0,0 @@
|
||||||
#ifndef SINGLECOREAPPLICATION_H
|
|
||||||
#define SINGLECOREAPPLICATION_H
|
|
||||||
|
|
||||||
#ifdef SINGLECOREAPPLICATION
|
|
||||||
# error "SINGLECOREAPPLICATION already defined."
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define SINGLECOREAPPLICATION
|
|
||||||
#include "../singleapplication_t.h"
|
|
||||||
#undef SINGLEAPPLICATION_T_H
|
|
||||||
#undef SINGLECOREAPPLICATION
|
|
||||||
|
|
||||||
#endif // SINGLECOREAPPLICATION_H
|
|
|
@ -328,13 +328,10 @@ if(NOT TAGLIB_FOUND AND NOT TAGPARSER_FOUND)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# SingleApplication
|
# SingleApplication
|
||||||
add_subdirectory(3rdparty/singleapplication)
|
add_subdirectory(3rdparty/kdsingleapplication)
|
||||||
set(SINGLEAPPLICATION_INCLUDE_DIRS
|
set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/kdsingleapplication)
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singleapplication
|
set(SINGLEAPPLICATION_LIBRARIES kdsingleapplication)
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singlecoreapplication
|
add_definitions(-DKDSINGLEAPPLICATION_STATIC_BUILD)
|
||||||
)
|
|
||||||
set(SINGLEAPPLICATION_LIBRARIES singleapplication)
|
|
||||||
set(SINGLECOREAPPLICATION_LIBRARIES singlecoreapplication)
|
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
add_subdirectory(3rdparty/SPMediaKeyTap)
|
add_subdirectory(3rdparty/SPMediaKeyTap)
|
||||||
|
|
|
@ -267,8 +267,8 @@ Copyright: 2010, Spotify AB
|
||||||
2011, Joachim Bengtsson
|
2011, Joachim Bengtsson
|
||||||
License: BSD-3-clause
|
License: BSD-3-clause
|
||||||
|
|
||||||
Files: 3rdparty/singleapplication/*
|
Files: 3rdparty/kdsingleapplication/*
|
||||||
Copyright: 2015-2022, Itay Grudev
|
Copyright: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
License: MIT
|
License: MIT
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -972,7 +972,6 @@ link_directories(
|
||||||
${SQLITE_LIBRARY_DIRS}
|
${SQLITE_LIBRARY_DIRS}
|
||||||
${PROTOBUF_LIBRARY_DIRS}
|
${PROTOBUF_LIBRARY_DIRS}
|
||||||
${SINGLEAPPLICATION_LIBRARY_DIRS}
|
${SINGLEAPPLICATION_LIBRARY_DIRS}
|
||||||
${SINGLECOREAPPLICATION_LIBRARY_DIRS}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if(HAVE_ICU)
|
if(HAVE_ICU)
|
||||||
|
@ -1080,7 +1079,6 @@ target_include_directories(strawberry_lib PUBLIC
|
||||||
${CMAKE_SOURCE_DIR}/ext/libstrawberry-tagreader
|
${CMAKE_SOURCE_DIR}/ext/libstrawberry-tagreader
|
||||||
${CMAKE_BINARY_DIR}/ext/libstrawberry-tagreader
|
${CMAKE_BINARY_DIR}/ext/libstrawberry-tagreader
|
||||||
${SINGLEAPPLICATION_INCLUDE_DIRS}
|
${SINGLEAPPLICATION_INCLUDE_DIRS}
|
||||||
${SINGLECOREAPPLICATION_INCLUDE_DIRS}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(strawberry_lib PUBLIC
|
target_link_libraries(strawberry_lib PUBLIC
|
||||||
|
@ -1092,7 +1090,6 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||||
${QT_LIBRARIES}
|
${QT_LIBRARIES}
|
||||||
${Protobuf_LIBRARIES}
|
${Protobuf_LIBRARIES}
|
||||||
${SINGLEAPPLICATION_LIBRARIES}
|
${SINGLEAPPLICATION_LIBRARIES}
|
||||||
${SINGLECOREAPPLICATION_LIBRARIES}
|
|
||||||
libstrawberry-common
|
libstrawberry-common
|
||||||
libstrawberry-tagreader
|
libstrawberry-tagreader
|
||||||
)
|
)
|
||||||
|
|
|
@ -2330,9 +2330,7 @@ void MainWindow::PlaylistEditFinished(const int playlist_id, const QModelIndex &
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::CommandlineOptionsReceived(const quint32 instanceId, const QByteArray &string_options) {
|
void MainWindow::CommandlineOptionsReceived(const QByteArray &string_options) {
|
||||||
|
|
||||||
Q_UNUSED(instanceId);
|
|
||||||
|
|
||||||
CommandlineOptions options;
|
CommandlineOptions options;
|
||||||
options.Load(string_options);
|
options.Load(string_options);
|
||||||
|
@ -2342,8 +2340,9 @@ void MainWindow::CommandlineOptionsReceived(const quint32 instanceId, const QByt
|
||||||
show();
|
show();
|
||||||
activateWindow();
|
activateWindow();
|
||||||
hidden_ = false;
|
hidden_ = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
CommandlineOptionsReceived(options);
|
CommandlineOptionsReceived(options);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -272,7 +272,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||||
void FocusSearchField();
|
void FocusSearchField();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void CommandlineOptionsReceived(const quint32 instanceId, const QByteArray &string_options);
|
void CommandlineOptionsReceived(const QByteArray &string_options);
|
||||||
void Raise();
|
void Raise();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
22
src/main.cpp
22
src/main.cpp
|
@ -66,8 +66,7 @@
|
||||||
|
|
||||||
#include "core/logging.h"
|
#include "core/logging.h"
|
||||||
|
|
||||||
#include <singleapplication.h>
|
#include <kdsingleapplication.h>
|
||||||
#include <singlecoreapplication.h>
|
|
||||||
|
|
||||||
#ifdef HAVE_QTSPARKLE
|
#ifdef HAVE_QTSPARKLE
|
||||||
# if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
# if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||||
|
@ -148,15 +147,16 @@ int main(int argc, char *argv[]) {
|
||||||
{
|
{
|
||||||
// Only start a core application now, so we can check if there's another instance without requiring an X server.
|
// Only start a core application now, so we can check if there's another instance without requiring an X server.
|
||||||
// This MUST be done before parsing the commandline options so QTextCodec gets the right system locale for filenames.
|
// This MUST be done before parsing the commandline options so QTextCodec gets the right system locale for filenames.
|
||||||
SingleCoreApplication core_app(argc, argv, true, SingleCoreApplication::Mode::User | SingleCoreApplication::Mode::ExcludeAppVersion | SingleCoreApplication::Mode::ExcludeAppPath);
|
QCoreApplication core_app(argc, argv);
|
||||||
|
KDSingleApplication single_app(QCoreApplication::applicationName());
|
||||||
// Parse commandline options - need to do this before starting the full QApplication, so it works without an X server
|
// Parse commandline options - need to do this before starting the full QApplication, so it works without an X server
|
||||||
if (!options.Parse()) return 1;
|
if (!options.Parse()) return 1;
|
||||||
logging::SetLevels(options.log_levels());
|
logging::SetLevels(options.log_levels());
|
||||||
if (core_app.isSecondary()) {
|
if (!single_app.isPrimaryInstance()) {
|
||||||
if (options.is_empty()) {
|
if (options.is_empty()) {
|
||||||
qLog(Info) << "Strawberry is already running - activating existing window (1)";
|
qLog(Info) << "Strawberry is already running - activating existing window (1)";
|
||||||
}
|
}
|
||||||
if (!core_app.sendMessage(options.Serialize(), 5000)) {
|
if (!single_app.sendMessage(options.Serialize())) {
|
||||||
qLog(Error) << "Could not send message to primary instance.";
|
qLog(Error) << "Could not send message to primary instance.";
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -182,14 +182,13 @@ int main(int argc, char *argv[]) {
|
||||||
|
|
||||||
QGuiApplication::setQuitOnLastWindowClosed(false);
|
QGuiApplication::setQuitOnLastWindowClosed(false);
|
||||||
|
|
||||||
// important: Do not remove this.
|
QApplication a(argc, argv);
|
||||||
// This must also be done as a SingleApplication, in case SingleCoreApplication was compiled with a different appdata.
|
KDSingleApplication single_app(QCoreApplication::applicationName());
|
||||||
SingleApplication a(argc, argv, true, SingleApplication::Mode::User | SingleApplication::Mode::ExcludeAppVersion | SingleApplication::Mode::ExcludeAppPath);
|
if (!single_app.isPrimaryInstance()) {
|
||||||
if (a.isSecondary()) {
|
|
||||||
if (options.is_empty()) {
|
if (options.is_empty()) {
|
||||||
qLog(Info) << "Strawberry is already running - activating existing window (2)";
|
qLog(Info) << "Strawberry is already running - activating existing window (2)";
|
||||||
}
|
}
|
||||||
if (!a.sendMessage(options.Serialize(), 5000)) {
|
if (!single_app.sendMessage(options.Serialize())) {
|
||||||
qLog(Error) << "Could not send message to primary instance.";
|
qLog(Error) << "Could not send message to primary instance.";
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -319,9 +318,10 @@ int main(int argc, char *argv[]) {
|
||||||
#ifdef HAVE_DBUS
|
#ifdef HAVE_DBUS
|
||||||
QObject::connect(&mpris2, &mpris::Mpris2::RaiseMainWindow, &w, &MainWindow::Raise);
|
QObject::connect(&mpris2, &mpris::Mpris2::RaiseMainWindow, &w, &MainWindow::Raise);
|
||||||
#endif
|
#endif
|
||||||
QObject::connect(&a, &SingleApplication::receivedMessage, &w, QOverload<quint32, const QByteArray&>::of(&MainWindow::CommandlineOptionsReceived));
|
QObject::connect(&single_app, &KDSingleApplication::messageReceived, &w, QOverload<const QByteArray&>::of(&MainWindow::CommandlineOptionsReceived));
|
||||||
|
|
||||||
int ret = QCoreApplication::exec();
|
int ret = QCoreApplication::exec();
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue