Update SingleApplication

This commit is contained in:
Jonas Kvinge 2022-02-06 00:07:15 +01:00
parent 79f0c494fa
commit 1dd4eb05f2
10 changed files with 390 additions and 231 deletions

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) Itay Grudev 2015 - 2016
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

View File

@ -1,7 +1,8 @@
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`.
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)
@ -15,18 +16,6 @@ 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.
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.
You can use the library as if you use any other `QCoreApplication` derived
class:
@ -43,24 +32,49 @@ int main( int argc, char* argv[] )
```
To include the library files I would recommend that you add it as a git
submodule to your project and include it's contents with a `.pri` file. Here is
how:
submodule to your project. Here is how:
```bash
git submodule add git@github.com:itay-grudev/SingleApplication.git singleapplication
git submodule add https://github.com/itay-grudev/SingleApplication.git singleapplication
```
Then include the `singleapplication.pri` file in your `.pro` project file. Also
don't forget to specify which `QCoreApplication` class your app is using if it
is not `QCoreApplication`.
**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
@ -125,13 +139,22 @@ 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 )
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
@ -140,7 +163,7 @@ 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.
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.*
@ -159,7 +182,8 @@ 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
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`.
---
@ -192,6 +216,22 @@ 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

View File

@ -54,7 +54,7 @@
* @param options Optional flags to toggle specific behaviour
* @param timeout Maximum time blocking functions are allowed during app load
*/
SingleApplication::SingleApplication(int &argc, char *argv[], bool allowSecondary, Options options, int timeout)
SingleApplication::SingleApplication(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
: app_t(argc, argv),
d_ptr(new SingleApplicationPrivate(this)) {
@ -67,7 +67,7 @@ SingleApplication::SingleApplication(int &argc, char *argv[], bool allowSecondar
d->genBlockServerName();
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
d->randomSleep();
SingleApplicationPrivate::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.
@ -106,14 +106,14 @@ SingleApplication::SingleApplication(int &argc, char *argv[], bool allowSecondar
}
}
InstancesInfo *inst = static_cast<InstancesInfo*>(d->memory_->data());
InstancesInfo *instance = static_cast<InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
forever {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == inst->checksum) break;
if (d->blockChecksum() == instance->checksum) break;
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position
if (time.elapsed() > 5000) {
@ -127,14 +127,14 @@ SingleApplication::SingleApplication(int &argc, char *argv[], bool allowSecondar
qDebug() << "SingleApplication: Unable to unlock memory for random wait.";
qDebug() << d->memory_->errorString();
}
d->randomSleep();
SingleApplicationPrivate::randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if (!inst->primary) {
if (!instance->primary) {
d->startPrimary();
if (!d->memory_->unlock()) {
qDebug() << "SingleApplication: Unable to unlock memory after primary start.";
@ -178,8 +178,8 @@ SingleApplication::~SingleApplication() {
* Checks if the current application instance is primary.
* @return Returns true if the instance is primary, false otherwise.
*/
bool SingleApplication::isPrimary() {
Q_D(SingleApplication);
bool SingleApplication::isPrimary() const {
Q_D(const SingleApplication);
return d->server_ != nullptr;
}
@ -187,8 +187,8 @@ bool SingleApplication::isPrimary() {
* Checks if the current application instance is secondary.
* @return Returns true if the instance is secondary, false otherwise.
*/
bool SingleApplication::isSecondary() {
Q_D(SingleApplication);
bool SingleApplication::isSecondary() const {
Q_D(const SingleApplication);
return d->server_ == nullptr;
}
@ -197,8 +197,8 @@ bool SingleApplication::isSecondary() {
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
* @return Returns a unique instance id.
*/
quint32 SingleApplication::instanceId() {
Q_D(SingleApplication);
quint32 SingleApplication::instanceId() const {
Q_D(const SingleApplication);
return d->instanceNumber_;
}
@ -207,8 +207,8 @@ quint32 SingleApplication::instanceId() {
* Especially useful when SingleApplication is coupled with OS. specific APIs.
* @return Returns the primary instance PID.
*/
qint64 SingleApplication::primaryPid() {
Q_D(SingleApplication);
qint64 SingleApplication::primaryPid() const {
Q_D(const SingleApplication);
return d->primaryPid();
}
@ -216,8 +216,8 @@ qint64 SingleApplication::primaryPid() {
* Returns the username the primary instance is running as.
* @return Returns the username the primary instance is running as.
*/
QString SingleApplication::primaryUser() {
Q_D(SingleApplication);
QString SingleApplication::primaryUser() const {
Q_D(const SingleApplication);
return d->primaryUser();
}
@ -225,9 +225,8 @@ QString SingleApplication::primaryUser() {
* Returns the username the current instance is running as.
* @return Returns the username the current instance is running as.
*/
QString SingleApplication::currentUser() {
Q_D(SingleApplication);
return d->getUsername();
QString SingleApplication::currentUser() const {
return SingleApplicationPrivate::getUsername();
}
/**
@ -248,11 +247,7 @@ bool SingleApplication::sendMessage(const QByteArray &message, const int timeout
return false;
}
d->socket_->write(message);
const bool dataWritten = d->socket_->waitForBytesWritten(timeout);
d->socket_->flush();
return dataWritten;
return d->writeConfirmedMessage(timeout, message);
}
/**
@ -265,6 +260,7 @@ void SingleApplication::abortSafely() {
qCritical() << "SingleApplication: " << d->memory_->error() << d->memory_->errorString();
delete d;
::exit(EXIT_FAILURE);
}

View File

@ -48,7 +48,7 @@ class SingleApplicationPrivate;
class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-parent-argument
Q_OBJECT
typedef QApplication app_t;
using app_t = QApplication;
public:
/**
@ -88,46 +88,45 @@ class SingleApplication : public QApplication { // clazy:exclude=ctor-missing-p
* operations. It does not guarantee that the SingleApplication
* initialisation will be completed in given time, though is a good hint.
* Usually 4*timeout would be the worst case (fail) scenario.
* @see See the corresponding QAPPLICATION_CLASS constructor for reference
*/
explicit SingleApplication(int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000);
explicit SingleApplication(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);
~SingleApplication() override;
/**
* @brief Returns if the instance is the primary instance
* @returns {bool}
*/
bool isPrimary();
bool isPrimary() const;
/**
* @brief Returns if the instance is a secondary instance
* @returns {bool}
*/
bool isSecondary();
bool isSecondary() const;
/**
* @brief Returns a unique identifier for the current instance
* @returns {qint32}
*/
quint32 instanceId();
quint32 instanceId() const;
/**
* @brief Returns the process ID (PID) of the primary instance
* @returns {qint64}
*/
qint64 primaryPid();
qint64 primaryPid() const;
/**
* @brief Returns the username of the user running the primary instance
* @returns {QString}
*/
QString primaryUser();
QString primaryUser() const;
/**
* @brief Returns the username of the current user
* @returns {QString}
*/
QString currentUser();
QString currentUser() const;
/**
* @brief Sends a message to the primary instance. Returns true on success.

View File

@ -44,6 +44,14 @@
# 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>
@ -53,7 +61,6 @@
#include <QCryptographicHash>
#include <QLocalServer>
#include <QLocalSocket>
#include <QDir>
#include <QElapsedTimer>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
@ -64,11 +71,6 @@
#include "singleapplication.h"
#include "singleapplication_p.h"
#ifdef Q_OS_WIN
# include <windows.h>
# include <lmcons.h>
#endif
SingleApplicationPrivate::SingleApplicationPrivate(SingleApplication *ptr)
: q_ptr(ptr),
memory_(nullptr),
@ -86,14 +88,14 @@ SingleApplicationPrivate::~SingleApplicationPrivate() {
if (memory_ != nullptr) {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) {
server_->close();
delete server_;
inst->primary = false;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
instance->primary = false;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
memory_->unlock();
@ -152,7 +154,15 @@ void SingleApplicationPrivate::genBlockServerName() {
}
if (!(options_ & SingleApplication::Mode::ExcludeAppPath)) {
#ifdef Q_OS_WIN
#if defined(Q_OS_UNIX)
const QByteArray appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
appData.addData(SingleApplication::app_t::applicationFilePath().toUtf8());
}
else {
appData.addData(appImagePath);
};
#elif defined(Q_OS_WIN)
appData.addData(SingleApplication::app_t::applicationFilePath().toLower().toUtf8());
#else
appData.addData(SingleApplication::app_t::applicationFilePath().toUtf8());
@ -171,26 +181,24 @@ void SingleApplicationPrivate::genBlockServerName() {
void SingleApplicationPrivate::initializeMemoryBlock() const {
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
inst->primary = false;
inst->secondary = 0;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = false;
instance->secondary = 0;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
void SingleApplicationPrivate::startPrimary() {
Q_Q(SingleApplication);
// Reset the number of connections
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
inst->primary = true;
inst->primaryPid = q->applicationPid();
qstrncpy(inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser));
inst->checksum = blockChecksum();
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
@ -212,11 +220,11 @@ void SingleApplicationPrivate::startPrimary() {
void SingleApplicationPrivate::startSecondary() {
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
inst->secondary += 1;
inst->checksum = blockChecksum();
instanceNumber_ = inst->secondary;
instance->secondary += 1;
instance->checksum = blockChecksum();
instanceNumber_ = instance->secondary;
}
@ -263,25 +271,53 @@ bool SingleApplicationPrivate::connectToPrimary(const int timeout, const Connect
writeStream << instanceNumber_;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
#else
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
#endif
writeStream << checksum;
// The header indicates the message length that follows
return writeConfirmedMessage(static_cast<int>(timeout - time.elapsed()), initMsg);
}
void SingleApplicationPrivate::writeAck(QLocalSocket *sock) {
sock->putChar('\n');
}
bool SingleApplicationPrivate::writeConfirmedMessage(const int timeout, const QByteArray &msg) {
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>(initMsg.length());
headerStream << static_cast<quint64>(msg.length());
socket_->write(header);
socket_->write(initMsg);
bool result = socket_->waitForBytesWritten(static_cast<int>(timeout - time.elapsed()));
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 SingleApplicationPrivate::writeConfirmedFrame(const int timeout, const QByteArray &msg) {
socket_->write(msg);
socket_->flush();
return result;
bool result = socket_->waitForReadyRead(timeout);
if (result) {
socket_->read(1);
return true;
}
return false;
}
@ -300,8 +336,8 @@ quint16 SingleApplicationPrivate::blockChecksum() const {
qint64 SingleApplicationPrivate::primaryPid() const {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = inst->primaryPid;
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = instance->primaryPid;
memory_->unlock();
return pid;
@ -311,8 +347,8 @@ qint64 SingleApplicationPrivate::primaryPid() const {
QString SingleApplicationPrivate::primaryUser() const {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = inst->primaryUser;
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = instance->primaryUser;
memory_->unlock();
return QString::fromUtf8(username);
@ -328,26 +364,30 @@ void SingleApplicationPrivate::slotConnectionEstablished() {
connectionMap_.insert(nextConnSocket, ConnectionInfo());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this]() {
const ConnectionInfo info = connectionMap_[nextConnSocket];
const ConnectionInfo &info = connectionMap_[nextConnSocket];
slotClientConnectionClosed(nextConnSocket, info.instanceId);
});
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, this, [nextConnSocket, this]() {
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this]() {
connectionMap_.remove(nextConnSocket);
nextConnSocket->deleteLater();
});
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this]() {
const ConnectionInfo info = connectionMap_[nextConnSocket];
const ConnectionInfo &info = connectionMap_[nextConnSocket];
switch (info.stage) {
case StageHeader:
readInitMessageHeader(nextConnSocket);
case StageInitHeader:
readMessageHeader(nextConnSocket, StageInitBody);
break;
case StageBody:
case StageInitBody:
readInitMessageBody(nextConnSocket);
break;
case StageConnected:
slotDataAvailable(nextConnSocket, info.instanceId);
case StageConnectedHeader:
readMessageHeader(nextConnSocket, StageConnectedBody);
break;
case StageConnectedBody:
this->slotDataAvailable(nextConnSocket, info.instanceId);
break;
default:
break;
@ -356,7 +396,7 @@ void SingleApplicationPrivate::slotConnectionEstablished() {
}
void SingleApplicationPrivate::readInitMessageHeader(QLocalSocket *sock) {
void SingleApplicationPrivate::readMessageHeader(QLocalSocket *sock, const SingleApplicationPrivate::ConnectionStage nextStage) {
if (!connectionMap_.contains(sock)) {
return;
@ -373,30 +413,38 @@ void SingleApplicationPrivate::readInitMessageHeader(QLocalSocket *sock) {
quint64 msgLen = 0;
headerStream >> msgLen;
ConnectionInfo &info = connectionMap_[sock];
info.stage = StageBody;
info.stage = nextStage;
info.msgLen = msgLen;
if (sock->bytesAvailable() >= static_cast<qint64>(msgLen)) {
readInitMessageBody(sock);
writeAck(sock);
}
bool SingleApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
if (!connectionMap_.contains(sock)) {
return false;
}
const ConnectionInfo &info = connectionMap_[sock];
if (sock->bytesAvailable() < static_cast<qint64>(info.msgLen)) {
return false;
}
return true;
}
void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
Q_Q(SingleApplication);
if (!connectionMap_.contains(sock)) {
return;
}
ConnectionInfo &info = connectionMap_[sock];
if (sock->bytesAvailable() < static_cast<qint64>(info.msgLen)) {
if (!isFrameComplete(sock)) {
return;
}
// Read the message body
QByteArray msgBytes = sock->read(static_cast<qint64>(info.msgLen));
QByteArray msgBytes = sock->readAll();
QDataStream readStream(msgBytes);
readStream.setVersion(QDataStream::Qt_5_8);
@ -431,23 +479,34 @@ void SingleApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
return;
}
ConnectionInfo &info = connectionMap_[sock];
info.instanceId = instanceId;
info.stage = StageConnected;
info.stage = StageConnectedHeader;
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplication::Mode::SecondaryNotification)) {
Q_EMIT q->instanceStarted();
emit q->instanceStarted();
}
if (sock->bytesAvailable() > 0) {
slotDataAvailable(sock, instanceId);
}
writeAck(sock);
}
void SingleApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
Q_Q(SingleApplication);
Q_EMIT q->receivedMessage(instanceId, dataSocket->readAll());
if (!isFrameComplete(dataSocket)) {
return;
}
const QByteArray message = dataSocket->readAll();
writeAck(dataSocket);
ConnectionInfo &info = connectionMap_[dataSocket];
info.stage = StageConnectedHeader;
emit q->receivedMessage(instanceId, message);
}
@ -465,7 +524,7 @@ void SingleApplicationPrivate::randomSleep() {
QThread::msleep(QRandomGenerator::global()->bounded(8U, 18U));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::msleep(8 + static_cast<unsigned long>(static_cast<float>(qrand()) / RAND_MAX * 10));
QThread::msleep(qrand() % 11 + 8);
#endif
}

View File

@ -71,9 +71,10 @@ class SingleApplicationPrivate : public QObject {
Reconnect = 3
};
enum ConnectionStage : quint8 {
StageHeader = 0,
StageBody = 1,
StageConnected = 2,
StageInitHeader = 0,
StageInitBody = 1,
StageConnectedHeader = 2,
StageConnectedBody = 3,
};
Q_DECLARE_PUBLIC(SingleApplication)
@ -89,8 +90,12 @@ class SingleApplicationPrivate : public QObject {
quint16 blockChecksum() const;
qint64 primaryPid() const;
QString primaryUser() const;
void readInitMessageHeader(QLocalSocket *socket);
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);
bool writeConfirmedMessage(const int timeout, const QByteArray &msg);
static void randomSleep();
SingleApplication *q_ptr;

View File

@ -54,7 +54,7 @@
* @param options Optional flags to toggle specific behaviour
* @param timeout Maximum time blocking functions are allowed during app load
*/
SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], bool allowSecondary, Options options, int timeout)
SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout)
: app_t(argc, argv),
d_ptr(new SingleCoreApplicationPrivate(this)) {
@ -67,7 +67,7 @@ SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], bool allow
d->genBlockServerName();
// To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time
d->randomSleep();
SingleCoreApplicationPrivate::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.
@ -106,14 +106,14 @@ SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], bool allow
}
}
InstancesInfo *inst = static_cast<InstancesInfo*>(d->memory_->data());
InstancesInfo *instance = static_cast<InstancesInfo*>(d->memory_->data());
QElapsedTimer time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
forever {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == inst->checksum) break;
if (d->blockChecksum() == instance->checksum) break;
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position
if (time.elapsed() > 5000) {
@ -127,14 +127,14 @@ SingleCoreApplication::SingleCoreApplication(int &argc, char *argv[], bool allow
qDebug() << "SingleCoreApplication: Unable to unlock memory for random wait.";
qDebug() << d->memory_->errorString();
}
d->randomSleep();
SingleCoreApplicationPrivate::randomSleep();
if (!d->memory_->lock()) {
qCritical() << "SingleCoreApplication: Unable to lock memory after random wait.";
abortSafely();
}
}
if (!inst->primary) {
if (!instance->primary) {
d->startPrimary();
if (!d->memory_->unlock()) {
qDebug() << "SingleCoreApplication: Unable to unlock memory after primary start.";
@ -178,8 +178,8 @@ SingleCoreApplication::~SingleCoreApplication() {
* Checks if the current application instance is primary.
* @return Returns true if the instance is primary, false otherwise.
*/
bool SingleCoreApplication::isPrimary() {
Q_D(SingleCoreApplication);
bool SingleCoreApplication::isPrimary() const {
Q_D(const SingleCoreApplication);
return d->server_ != nullptr;
}
@ -187,8 +187,8 @@ bool SingleCoreApplication::isPrimary() {
* Checks if the current application instance is secondary.
* @return Returns true if the instance is secondary, false otherwise.
*/
bool SingleCoreApplication::isSecondary() {
Q_D(SingleCoreApplication);
bool SingleCoreApplication::isSecondary() const {
Q_D(const SingleCoreApplication);
return d->server_ == nullptr;
}
@ -197,8 +197,8 @@ bool SingleCoreApplication::isSecondary() {
* It is reset when the first (primary) instance of your app starts and only incremented afterwards.
* @return Returns a unique instance id.
*/
quint32 SingleCoreApplication::instanceId() {
Q_D(SingleCoreApplication);
quint32 SingleCoreApplication::instanceId() const {
Q_D(const SingleCoreApplication);
return d->instanceNumber_;
}
@ -207,8 +207,8 @@ quint32 SingleCoreApplication::instanceId() {
* Especially useful when SingleCoreApplication is coupled with OS. specific APIs.
* @return Returns the primary instance PID.
*/
qint64 SingleCoreApplication::primaryPid() {
Q_D(SingleCoreApplication);
qint64 SingleCoreApplication::primaryPid() const {
Q_D(const SingleCoreApplication);
return d->primaryPid();
}
@ -216,8 +216,8 @@ qint64 SingleCoreApplication::primaryPid() {
* Returns the username the primary instance is running as.
* @return Returns the username the primary instance is running as.
*/
QString SingleCoreApplication::primaryUser() {
Q_D(SingleCoreApplication);
QString SingleCoreApplication::primaryUser() const {
Q_D(const SingleCoreApplication);
return d->primaryUser();
}
@ -225,9 +225,8 @@ QString SingleCoreApplication::primaryUser() {
* Returns the username the current instance is running as.
* @return Returns the username the current instance is running as.
*/
QString SingleCoreApplication::currentUser() {
Q_D(SingleCoreApplication);
return d->getUsername();
QString SingleCoreApplication::currentUser() const {
return SingleCoreApplicationPrivate::getUsername();
}
/**
@ -248,11 +247,7 @@ bool SingleCoreApplication::sendMessage(const QByteArray &message, const int tim
return false;
}
d->socket_->write(message);
const bool dataWritten = d->socket_->waitForBytesWritten(timeout);
d->socket_->flush();
return dataWritten;
return d->writeConfirmedMessage(timeout, message);
}
/**
@ -265,6 +260,7 @@ void SingleCoreApplication::abortSafely() {
qCritical() << "SingleCoreApplication: " << d->memory_->error() << d->memory_->errorString();
delete d;
::exit(EXIT_FAILURE);
}

View File

@ -45,10 +45,10 @@ class SingleCoreApplicationPrivate;
* @brief The SingleCoreApplication class handles multiple instances of the same Application
* @see QCoreApplication
*/
class SingleCoreApplication : public QCoreApplication {
class SingleCoreApplication : public QCoreApplication { // clazy:exclude=ctor-missing-parent-argument
Q_OBJECT
typedef QCoreApplication app_t;
using app_t = QCoreApplication;
public:
/**
@ -96,37 +96,37 @@ class SingleCoreApplication : public QCoreApplication {
* @brief Returns if the instance is the primary instance
* @returns {bool}
*/
bool isPrimary();
bool isPrimary() const;
/**
* @brief Returns if the instance is a secondary instance
* @returns {bool}
*/
bool isSecondary();
bool isSecondary() const;
/**
* @brief Returns a unique identifier for the current instance
* @returns {qint32}
*/
quint32 instanceId();
quint32 instanceId() const;
/**
* @brief Returns the process ID (PID) of the primary instance
* @returns {qint64}
*/
qint64 primaryPid();
qint64 primaryPid() const;
/**
* @brief Returns the username of the user running the primary instance
* @returns {QString}
*/
QString primaryUser();
QString primaryUser() const;
/**
* @brief Returns the username of the current user
* @returns {QString}
*/
QString currentUser();
QString currentUser() const;
/**
* @brief Sends a message to the primary instance. Returns true on success.

View File

@ -44,6 +44,14 @@
# 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>
@ -53,7 +61,6 @@
#include <QCryptographicHash>
#include <QLocalServer>
#include <QLocalSocket>
#include <QDir>
#include <QElapsedTimer>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
# include <QRandomGenerator>
@ -64,11 +71,6 @@
#include "singlecoreapplication.h"
#include "singlecoreapplication_p.h"
#ifdef Q_OS_WIN
# include <windows.h>
# include <lmcons.h>
#endif
SingleCoreApplicationPrivate::SingleCoreApplicationPrivate(SingleCoreApplication *ptr)
: q_ptr(ptr),
memory_(nullptr),
@ -86,14 +88,14 @@ SingleCoreApplicationPrivate::~SingleCoreApplicationPrivate() {
if (memory_ != nullptr) {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) {
server_->close();
delete server_;
inst->primary = false;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
instance->primary = false;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
memory_->unlock();
@ -152,7 +154,15 @@ void SingleCoreApplicationPrivate::genBlockServerName() {
}
if (!(options_ & SingleCoreApplication::Mode::ExcludeAppPath)) {
#ifdef Q_OS_WIN
#if defined(Q_OS_UNIX)
const QByteArray appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toUtf8());
}
else {
appData.addData(appImagePath);
};
#elif defined(Q_OS_WIN)
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toLower().toUtf8());
#else
appData.addData(SingleCoreApplication::app_t::applicationFilePath().toUtf8());
@ -171,26 +181,24 @@ void SingleCoreApplicationPrivate::genBlockServerName() {
void SingleCoreApplicationPrivate::initializeMemoryBlock() const {
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
inst->primary = false;
inst->secondary = 0;
inst->primaryPid = -1;
inst->primaryUser[0] = '\0';
inst->checksum = blockChecksum();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = false;
instance->secondary = 0;
instance->primaryPid = -1;
instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum();
}
void SingleCoreApplicationPrivate::startPrimary() {
Q_Q(SingleCoreApplication);
// Reset the number of connections
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
inst->primary = true;
inst->primaryPid = q->applicationPid();
qstrncpy(inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser));
inst->checksum = blockChecksum();
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
@ -212,11 +220,11 @@ void SingleCoreApplicationPrivate::startPrimary() {
void SingleCoreApplicationPrivate::startSecondary() {
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
inst->secondary += 1;
inst->checksum = blockChecksum();
instanceNumber_ = inst->secondary;
instance->secondary += 1;
instance->checksum = blockChecksum();
instanceNumber_ = instance->secondary;
}
@ -263,25 +271,53 @@ bool SingleCoreApplicationPrivate::connectToPrimary(const int timeout, const Con
writeStream << instanceNumber_;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
#else
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
#endif
writeStream << checksum;
// The header indicates the message length that follows
return writeConfirmedMessage(static_cast<int>(timeout - time.elapsed()), initMsg);
}
void SingleCoreApplicationPrivate::writeAck(QLocalSocket *sock) {
sock->putChar('\n');
}
bool SingleCoreApplicationPrivate::writeConfirmedMessage(const int timeout, const QByteArray &msg) {
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>(initMsg.length());
headerStream << static_cast<quint64>(msg.length());
socket_->write(header);
socket_->write(initMsg);
bool result = socket_->waitForBytesWritten(timeout - static_cast<int>(time.elapsed()));
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 SingleCoreApplicationPrivate::writeConfirmedFrame(const int timeout, const QByteArray &msg) {
socket_->write(msg);
socket_->flush();
return result;
bool result = socket_->waitForReadyRead(timeout);
if (result) {
socket_->read(1);
return true;
}
return false;
}
@ -300,8 +336,8 @@ quint16 SingleCoreApplicationPrivate::blockChecksum() const {
qint64 SingleCoreApplicationPrivate::primaryPid() const {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = inst->primaryPid;
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
qint64 pid = instance->primaryPid;
memory_->unlock();
return pid;
@ -311,8 +347,8 @@ qint64 SingleCoreApplicationPrivate::primaryPid() const {
QString SingleCoreApplicationPrivate::primaryUser() const {
memory_->lock();
InstancesInfo *inst = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = inst->primaryUser;
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
QByteArray username = instance->primaryUser;
memory_->unlock();
return QString::fromUtf8(username);
@ -328,26 +364,30 @@ void SingleCoreApplicationPrivate::slotConnectionEstablished() {
connectionMap_.insert(nextConnSocket, ConnectionInfo());
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, this]() {
const ConnectionInfo info = connectionMap_[nextConnSocket];
const ConnectionInfo &info = connectionMap_[nextConnSocket];
slotClientConnectionClosed(nextConnSocket, info.instanceId);
});
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, this, [nextConnSocket, this]() {
QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this]() {
connectionMap_.remove(nextConnSocket);
nextConnSocket->deleteLater();
});
QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, this]() {
const ConnectionInfo info = connectionMap_[nextConnSocket];
const ConnectionInfo &info = connectionMap_[nextConnSocket];
switch (info.stage) {
case StageHeader:
readInitMessageHeader(nextConnSocket);
case StageInitHeader:
readMessageHeader(nextConnSocket, StageInitBody);
break;
case StageBody:
case StageInitBody:
readInitMessageBody(nextConnSocket);
break;
case StageConnected:
slotDataAvailable(nextConnSocket, info.instanceId);
case StageConnectedHeader:
readMessageHeader(nextConnSocket, StageConnectedBody);
break;
case StageConnectedBody:
this->slotDataAvailable(nextConnSocket, info.instanceId);
break;
default:
break;
@ -356,7 +396,7 @@ void SingleCoreApplicationPrivate::slotConnectionEstablished() {
}
void SingleCoreApplicationPrivate::readInitMessageHeader(QLocalSocket *sock) {
void SingleCoreApplicationPrivate::readMessageHeader(QLocalSocket *sock, SingleCoreApplicationPrivate::ConnectionStage nextStage) {
if (!connectionMap_.contains(sock)) {
return;
@ -373,30 +413,38 @@ void SingleCoreApplicationPrivate::readInitMessageHeader(QLocalSocket *sock) {
quint64 msgLen = 0;
headerStream >> msgLen;
ConnectionInfo &info = connectionMap_[sock];
info.stage = StageBody;
info.stage = nextStage;
info.msgLen = msgLen;
if (sock->bytesAvailable() >= static_cast<qint64>(msgLen)) {
readInitMessageBody(sock);
writeAck(sock);
}
bool SingleCoreApplicationPrivate::isFrameComplete(QLocalSocket *sock) {
if (!connectionMap_.contains(sock)) {
return false;
}
ConnectionInfo &info = connectionMap_[sock];
if (sock->bytesAvailable() < static_cast<qint64>(info.msgLen)) {
return false;
}
return true;
}
void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
Q_Q(SingleCoreApplication);
if (!connectionMap_.contains(sock)) {
return;
}
ConnectionInfo &info = connectionMap_[sock];
if (sock->bytesAvailable() < static_cast<qint64>(info.msgLen)) {
if (!isFrameComplete(sock)) {
return;
}
// Read the message body
QByteArray msgBytes = sock->read(static_cast<qint64>(info.msgLen));
QByteArray msgBytes = sock->readAll();
QDataStream readStream(msgBytes);
readStream.setVersion(QDataStream::Qt_5_8);
@ -431,23 +479,34 @@ void SingleCoreApplicationPrivate::readInitMessageBody(QLocalSocket *sock) {
return;
}
ConnectionInfo &info = connectionMap_[sock];
info.instanceId = instanceId;
info.stage = StageConnected;
info.stage = StageConnectedHeader;
if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleCoreApplication::Mode::SecondaryNotification)) {
Q_EMIT q->instanceStarted();
emit q->instanceStarted();
}
if (sock->bytesAvailable() > 0) {
slotDataAvailable(sock, instanceId);
}
writeAck(sock);
}
void SingleCoreApplicationPrivate::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) {
Q_Q(SingleCoreApplication);
Q_EMIT q->receivedMessage(instanceId, dataSocket->readAll());
if (!isFrameComplete(dataSocket)) {
return;
}
const QByteArray message = dataSocket->readAll();
writeAck(dataSocket);
ConnectionInfo &info = connectionMap_[dataSocket];
info.stage = StageConnectedHeader;
emit q->receivedMessage(instanceId, message);
}
@ -465,7 +524,7 @@ void SingleCoreApplicationPrivate::randomSleep() {
QThread::msleep(QRandomGenerator::global()->bounded(8U, 18U));
#else
qsrand(QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max());
QThread::msleep(8 + static_cast<unsigned long>(static_cast<float>(qrand()) / RAND_MAX * 10));
QThread::msleep(qrand() % 11 + 8);
#endif
}

View File

@ -71,9 +71,10 @@ class SingleCoreApplicationPrivate : public QObject {
Reconnect = 3
};
enum ConnectionStage : quint8 {
StageHeader = 0,
StageBody = 1,
StageConnected = 2,
StageInitHeader = 0,
StageInitBody = 1,
StageConnectedHeader = 2,
StageConnectedBody = 3,
};
Q_DECLARE_PUBLIC(SingleCoreApplication)
@ -89,8 +90,12 @@ class SingleCoreApplicationPrivate : public QObject {
quint16 blockChecksum() const;
qint64 primaryPid() const;
QString primaryUser() const;
void readInitMessageHeader(QLocalSocket *socket);
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);
bool writeConfirmedMessage(const int timeout, const QByteArray &msg);
static void randomSleep();
SingleCoreApplication *q_ptr;