SingleApplication: Refactor startup code

This commit is contained in:
Jonas Kvinge 2023-03-04 23:47:36 +01:00
parent 9ae0b32318
commit 6515e06a13
4 changed files with 115 additions and 117 deletions

View File

@ -80,27 +80,24 @@ SingleApplicationPrivateClass::SingleApplicationPrivateClass(SingleApplicationCl
SingleApplicationPrivateClass::~SingleApplicationPrivateClass() { SingleApplicationPrivateClass::~SingleApplicationPrivateClass() {
if (socket_ != nullptr) { if (socket_ != nullptr && socket_->isOpen()) {
socket_->close(); socket_->close();
delete socket_;
socket_ = nullptr;
} }
if (memory_ != nullptr) { if (memory_ != nullptr) {
memory_->lock(); memory_->lock();
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
if (server_ != nullptr) { if (server_ != nullptr) {
server_->close(); server_->close();
delete server_; InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
instance->primary = false; instance->primary = false;
instance->primaryPid = -1; instance->primaryPid = -1;
instance->primaryUser[0] = '\0'; instance->primaryUser[0] = '\0';
instance->checksum = blockChecksum(); instance->checksum = blockChecksum();
} }
memory_->unlock(); memory_->unlock();
if (memory_->isAttached()) {
delete memory_; memory_->detach();
memory_ = nullptr; }
} }
} }
@ -203,7 +200,7 @@ void SingleApplicationPrivateClass::startPrimary() {
// Successful creation means that no main process exists // Successful creation means that no main process exists
// So we start a QLocalServer to listen for connections // So we start a QLocalServer to listen for connections
QLocalServer::removeServer(blockServerName_); QLocalServer::removeServer(blockServerName_);
server_ = new QLocalServer(); server_ = new QLocalServer(this);
// Restrict access to the socket according to the SingleApplication::Mode::User flag on User level or no restrictions // Restrict access to the socket according to the SingleApplication::Mode::User flag on User level or no restrictions
if (options_ & SingleApplicationClass::Mode::User) { if (options_ & SingleApplicationClass::Mode::User) {
@ -230,16 +227,16 @@ void SingleApplicationPrivateClass::startSecondary() {
bool SingleApplicationPrivateClass::connectToPrimary(const int timeout, const ConnectionType connectionType) { bool SingleApplicationPrivateClass::connectToPrimary(const int timeout, const ConnectionType connectionType) {
QElapsedTimer time;
time.start();
// Connect to the Local Server of the Primary Instance if not already connected. // Connect to the Local Server of the Primary Instance if not already connected.
if (socket_ == nullptr) { if (socket_ == nullptr) {
socket_ = new QLocalSocket(); socket_ = new QLocalSocket(this);
} }
if (socket_->state() == QLocalSocket::ConnectedState) return true; if (socket_->state() == QLocalSocket::ConnectedState) return true;
QElapsedTimer time;
time.start();
if (socket_->state() != QLocalSocket::ConnectedState) { if (socket_->state() != QLocalSocket::ConnectedState) {
forever { forever {
@ -261,7 +258,7 @@ bool SingleApplicationPrivateClass::connectToPrimary(const int timeout, const Co
} }
} }
// Initialisation message according to the SingleApplication protocol // Initialization message according to the SingleApplication protocol
QByteArray initMsg; QByteArray initMsg;
QDataStream writeStream(&initMsg, QIODevice::WriteOnly); QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
writeStream.setVersion(QDataStream::Qt_5_8); writeStream.setVersion(QDataStream::Qt_5_8);

View File

@ -45,25 +45,13 @@ class QLocalServer;
class QLocalSocket; class QLocalSocket;
class QSharedMemory; class QSharedMemory;
struct InstancesInfo {
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;
};
class SingleApplicationPrivateClass : public QObject { class SingleApplicationPrivateClass : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit SingleApplicationPrivateClass(SingleApplicationClass *ptr);
~SingleApplicationPrivateClass() override;
enum ConnectionType : quint8 { enum ConnectionType : quint8 {
InvalidConnection = 0, InvalidConnection = 0,
NewInstance = 1, NewInstance = 1,
@ -74,12 +62,25 @@ class SingleApplicationPrivateClass : public QObject {
StageInitHeader = 0, StageInitHeader = 0,
StageInitBody = 1, StageInitBody = 1,
StageConnectedHeader = 2, StageConnectedHeader = 2,
StageConnectedBody = 3, StageConnectedBody = 3
}; };
Q_DECLARE_PUBLIC(SingleApplicationClass) Q_DECLARE_PUBLIC(SingleApplicationClass)
explicit SingleApplicationPrivateClass(SingleApplicationClass *ptr); struct InstancesInfo {
~SingleApplicationPrivateClass() override; 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(); static QString getUsername();
void genBlockServerName(); void genBlockServerName();

View File

@ -34,6 +34,8 @@
#include <cstdlib> #include <cstdlib>
#include <limits> #include <limits>
#include <boost/scope_exit.hpp>
#include <QtGlobal> #include <QtGlobal>
#include <QThread> #include <QThread>
#include <QSharedMemory> #include <QSharedMemory>
@ -77,108 +79,125 @@ SingleApplicationClass::SingleApplicationClass(int &argc, char *argv[], const bo
#ifdef Q_OS_UNIX #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. // 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) {
d->memory_ = new QSharedMemory(QNativeIpcKey(d->blockServerName_)); # if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
#else std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(QNativeIpcKey(d->blockServerName_));
d->memory_ = new QSharedMemory(d->blockServerName_); # else
#endif std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(d->blockServerName_);
d->memory_->attach(); # endif
delete d->memory_; if (memory->attach()) {
memory->detach();
}
}
#endif #endif
// Guarantee thread safe behaviour with a shared memory block. // Guarantee thread safe behaviour with a shared memory block.
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
d->memory_ = new QSharedMemory(QNativeIpcKey(d->blockServerName_)); QSharedMemory *memory = new QSharedMemory(QNativeIpcKey(d->blockServerName_), this);
#else #else
d->memory_ = new QSharedMemory(d->blockServerName_); QSharedMemory *memory = new QSharedMemory(d->blockServerName_, this);
#endif #endif
d->memory_ = memory;
bool primary = false;
// Create a shared memory block // Create a shared memory block
if (d->memory_->create(sizeof(InstancesInfo))) { if (d->memory_->create(sizeof(SingleApplicationPrivateClass::InstancesInfo))) {
// Initialize the shared memory block primary = true;
if (!d->memory_->lock()) { }
qCritical() << "SingleApplication: Unable to lock memory block after create."; else if (d->memory_->error() == QSharedMemory::AlreadyExists) {
abortSafely(); if (!d->memory_->attach()) {
qCritical() << "SingleApplication: Unable to attach to shared memory block:" << d->memory_->error() << d->memory_->errorString();
return;
} }
d->initializeMemoryBlock();
} }
else { else {
if (d->memory_->error() == QSharedMemory::AlreadyExists) { qCritical() << "SingleApplication: Unable to create shared memory block:" << d->memory_->error() << d->memory_->errorString();
// Attempt to attach to the memory segment return;
if (!d->memory_->attach()) {
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
abortSafely();
}
if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory block after attach.";
abortSafely();
}
}
else {
qCritical() << "SingleApplication: Unable to create block.";
abortSafely();
}
} }
InstancesInfo *instance = static_cast<InstancesInfo*>(d->memory_->data()); 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; QElapsedTimer time;
time.start(); time.start();
// Make sure the shared memory block is initialised and in consistent state // Make sure the shared memory block is initialized and in a consistent state
forever { while (d->blockChecksum() != instance->checksum) {
// If the shared memory block's checksum is valid continue
if (d->blockChecksum() == instance->checksum) break;
// If more than 5s have elapsed, assume the primary instance crashed and assume it's position // If more than 5 seconds have elapsed, assume the primary instance crashed and assume its position
if (time.elapsed() > 5000) { if (time.elapsed() > 5000) {
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure."; qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5 seconds. Assuming primary instance failure.";
d->initializeMemoryBlock(); d->initializeMemoryBlock();
} }
// Otherwise wait for a random period and try again. // 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 initialise faster // The random sleep here limits the probability of a collision between two racing apps and allows the app to initialize faster
if (!d->memory_->unlock()) { if (locked) {
qDebug() << "SingleApplication: Unable to unlock memory for random wait."; if (d->memory_->unlock()) {
qDebug() << d->memory_->errorString(); locked = false;
}
else {
qCritical() << "SingleApplication: Unable to unlock shared memory block for random wait:" << memory->error() << memory->errorString();
return;
}
} }
SingleApplicationPrivateClass::randomSleep(); SingleApplicationPrivateClass::randomSleep();
if (!d->memory_->lock()) { if (!d->memory_->lock()) {
qCritical() << "SingleApplication: Unable to lock memory after random wait."; qCritical() << "SingleApplication: Unable to lock shared memory block after random wait:" << memory->error() << memory->errorString();
abortSafely(); return;
} }
locked = true;
} }
if (!instance->primary) { 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(); d->startPrimary();
if (!d->memory_->unlock()) { primary = true;
qDebug() << "SingleApplication: Unable to unlock memory after primary start.";
qDebug() << d->memory_->errorString();
}
return;
} }
// Check if another instance can be started if (locked) {
if (allowSecondary) { if (d->memory_->unlock()) {
d->startSecondary(); locked = false;
if (d->options_ & Mode::SecondaryNotification) {
d->connectToPrimary(timeout, SingleApplicationPrivateClass::SecondaryInstance);
} }
if (!d->memory_->unlock()) { else {
qDebug() << "SingleApplication: Unable to unlock memory after secondary start."; qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString();
qDebug() << d->memory_->errorString();
} }
return;
} }
if (!d->memory_->unlock()) { if (!primary && !allowSecondary) {
qDebug() << "SingleApplication: Unable to unlock memory at end of execution."; d->connectToPrimary(timeout, SingleApplicationPrivateClass::NewInstance);
qDebug() << d->memory_->errorString();
} }
d->connectToPrimary(timeout, SingleApplicationPrivateClass::NewInstance);
delete d;
} }
SingleApplicationClass::~SingleApplicationClass() { SingleApplicationClass::~SingleApplicationClass() {
@ -308,22 +327,3 @@ bool SingleApplicationClass::sendMessage(const QByteArray &message, const int ti
return d->writeConfirmedMessage(timeout, message); return d->writeConfirmedMessage(timeout, message);
} }
/**
* Cleans up the shared memory block and exits with a failure.
* This function halts program execution.
*/
void SingleApplicationClass::abortSafely() {
#if defined(SINGLEAPPLICATION)
Q_D(SingleApplication);
#elif defined(SINGLECOREAPPLICATION)
Q_D(SingleCoreApplication);
#endif
qCritical() << "SingleApplication: " << d->memory_->error() << d->memory_->errorString();
delete d;
::exit(EXIT_FAILURE);
}

View File

@ -102,7 +102,7 @@ class SingleApplicationClass : public ApplicationClass { // clazy:exclude=ctor-
* instance and the secondary instance. * instance and the secondary instance.
* @note The timeout is just a hint for the maximum time of blocking * @note The timeout is just a hint for the maximum time of blocking
* operations. It does not guarantee that the SingleApplication * operations. It does not guarantee that the SingleApplication
* initialisation will be completed in given time, though is a good hint. * initialization will be completed in given time, though is a good hint.
* Usually 4*timeout would be the worst case (fail) scenario. * 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); explicit SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000);