From d15fa8feacfb4fe6494bb3160f8916eb2753c217 Mon Sep 17 00:00:00 2001 From: Itay Grudev Date: Mon, 20 May 2024 20:14:38 +0300 Subject: [PATCH] WIP implementation --- CMakeLists.txt | 3 +- TODO | 10 + message_coder.cpp | 235 ++++++++++++++++++++++ message_coder.h | 62 ++++++ singleapplication.cpp | 157 ++++----------- singleapplication.h | 27 +-- singleapplication.pri | 6 +- singleapplication_p.cpp | 372 ++++++----------------------------- singleapplication_p.h | 28 +-- singleapplicationmessage.cpp | 8 +- 10 files changed, 443 insertions(+), 465 deletions(-) create mode 100644 TODO create mode 100644 message_coder.cpp create mode 100644 message_coder.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 174d56c..f02f575 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,11 +7,12 @@ set(CMAKE_AUTOMOC ON) add_library(${PROJECT_NAME} STATIC singleapplication.cpp singleapplication_p.cpp + message_coder.cpp ) add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) if(NOT QT_DEFAULT_MAJOR_VERSION) - set(QT_DEFAULT_MAJOR_VERSION 5 CACHE STRING "Qt version to use (5 or 6), defaults to 5") + set(QT_DEFAULT_MAJOR_VERSION 6 CACHE STRING "Qt version to use (5 or 6), defaults to 5") endif() # Find dependencies diff --git a/TODO b/TODO new file mode 100644 index 0000000..e843c2b --- /dev/null +++ b/TODO @@ -0,0 +1,10 @@ +Implement all stubbed functions. +Add an instance counter that pings running secondary instances to ensure they are alive. +Run the entire server response logic in a thread, so the SingleApplication primary server is responsive independently of how busy the main thread of the app is. +Tests? + +REMOVE: + SingleApplicationPrivate::randomSleep(); + quint16 SingleApplicationPrivate::blockChecksum() + + Remove Mode::SecondaryNotification flag. A notification is always sent. diff --git a/message_coder.cpp b/message_coder.cpp new file mode 100644 index 0000000..2e6b10c --- /dev/null +++ b/message_coder.cpp @@ -0,0 +1,235 @@ +// Copyright (c) Itay Grudev 2023 +// +// 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: +// +// Permission is not granted to use this software or any of the associated files +// as sample data for the purposes of building machine learning models. +// +// 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. + +#include "message_coder.h" + +#include +#include + +MessageCoder::MessageCoder( QLocalSocket *socket ) + : socket(socket), dataStream( socket ) +{ + connect( socket, &QLocalSocket::readyRead, this, &MessageCoder::slotDataAvailable ); + + connect( socket, &QLocalSocket::aboutToClose, this, + [socket, this](){ + if( socket->bytesAvailable() > 0 ) + slotDataAvailable(); + } + ); +} + +void MessageCoder::slotDataAvailable() +{ + qDebug() << "slotDataAvailable()"; + struct { + quint8 magicNumber0; + quint8 magicNumber1; + quint8 magicNumber2; + quint8 magicNumber3; + quint32 protocolVersion; + SingleApplication::MessageType type; + quint16 instanceId; + qsizetype length; + QByteArray content; + quint16 checksum; + } msg; + + // An important note about the transaction mechanism: + // Rollback ends a transaction and resets the stream position to the start of the transaction so it can be + // retried if a packet was just incomplete. + // Abort on the other hand ends a transaction, but importantly does not reset the stream position, so it + // can be used to skip over a packet that is invalid and cannot be retried.1 + + while( socket->bytesAvailable() > 0 ){ + dataStream.startTransaction(); + + // The code below checks one byte at a time, so only one byte is consumed and skipped-over if the magic number + // doesn't match. Invalid magic numbers means that a message frame has not started so we abort the transaction. + dataStream >> msg.magicNumber0; + if( msg.magicNumber0 != 0x00 ){ + dataStream.abortTransaction(); + continue; + } + dataStream >> msg.magicNumber1; + if( msg.magicNumber1 != 0x01 ){ + dataStream.abortTransaction(); + continue; + } + dataStream >> msg.magicNumber2; + if( msg.magicNumber2 != 0x00 ){ + dataStream.abortTransaction(); + continue; + } + dataStream >> msg.magicNumber3; + if( msg.magicNumber3 != 0x02 ){ + dataStream.abortTransaction(); + continue; + } + + dataStream >> msg.protocolVersion; + if( msg.protocolVersion > 0x00000001 ){ + // An invalid protocol number means that the message cannot be be read, so we abort the transaction. + dataStream.abortTransaction(); + continue; + } + + dataStream >> msg.type; + switch( msg.type ){ + case SingleApplication::MessageType::Acknowledge: + case SingleApplication::MessageType::NewInstance: + case SingleApplication::MessageType::InstanceMessage: + break; + default: + // An invalid message type means that the message cannot be be read, so we abort the transaction. + dataStream.abortTransaction(); + continue; + } + + dataStream >> msg.instanceId; + + dataStream >> msg.length; // TODO: Consider adding a maximum message length check + qDebug() << "length:" << msg.length; + if( msg.length > 1024*1024 ){ // 1MiB + // An exceeded message length means that a message buffer should not be allocated, so we abort the transaction. + dataStream.abortTransaction(); + continue; + } + msg.content = QByteArray( msg.length, Qt::Uninitialized ); + int bytesRead = dataStream.readRawData( msg.content.data(), msg.length ); + if( bytesRead == -1 ){ + switch( dataStream.status() ){ + case QDataStream::ReadPastEnd: + // ReadPastEnd means and incomplete message so the message has not been transmitted fully. + // In this case we simply revert the transaction so it can be retried again later. + dataStream.rollbackTransaction(); + break; + case QDataStream::ReadCorruptData: + // Corrupted data means that the message cannot be be read, so we abort the transaction. + dataStream.abortTransaction(); + break; + default: + qWarning() << "Unexpected QDataStream status after readRawData:" << dataStream.status(); + dataStream.abortTransaction(); + break; + } + continue; + } else if( bytesRead != msg.length ){ + switch( dataStream.status() ){ + case QDataStream::Ok: + // Unexpected! Why a successful read did not read the expected number of bytes? Abort. + dataStream.abortTransaction(); + break; + case QDataStream::ReadPastEnd: + // ReadPastEnd means and incomplete message so the message has not been transmitted fully. + // In this case we simply revert the transaction so it can be retried again later. + dataStream.rollbackTransaction(); + break; + case QDataStream::ReadCorruptData: + // Corrupted data means that the message cannot be be read, so we abort the transaction. + dataStream.abortTransaction(); + break; + default: + qWarning() << "Unexpected QDataStream status in message length validation:" << dataStream.status(); + dataStream.abortTransaction(); + break; + } + continue; + } + + dataStream >> msg.checksum; + switch( dataStream.status() ){ + case QDataStream::Ok: + break; + case QDataStream::ReadPastEnd: + // ReadPastEnd means and incomplete message so the message has not been transmitted fully. + // In this case we simply revert the transaction so it can be retried again later. + dataStream.rollbackTransaction(); + break; + case QDataStream::ReadCorruptData: + // Corrupted data means that the message cannot be be read, so we abort the transaction. + dataStream.abortTransaction(); + break; + default: + // This could have been triggered by any of the preceeding read operations + qWarning() << "Unexpected QDataStream status:" << dataStream.status(); + dataStream.abortTransaction(); + break; + } + + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const quint16 computedChecksum = qChecksum(QByteArray(msg.content.constData(), static_cast(msg.content.length()))); + #else + const quint16 computedChecksum = qChecksum(msg.content.constData(), static_cast(msg.content.length())); + #endif + + if( msg.checksum != computedChecksum ){ + dataStream.abortTransaction(); + continue; + } + + if( dataStream.commitTransaction() ){ + qDebug() << "Message received:" << msg.type << msg.instanceId << msg.content; + messageReceived( + SingleApplication::Message { + .type = msg.type, + .instanceId = msg.instanceId, + .content = QByteArray( msg.content ) + } + ); + } + } +} + +bool MessageCoder::sendMessage( SingleApplication::MessageType type, quint16 instanceId, QByteArray content ) +{ + qDebug() << "sendMessage()"; + if( content.size() > 1024 * 1024 ){ // 1MiB + qWarning() << "Message content size exceeds maximum allowed size of 1MiB"; + return false; + } + + // See the latest: https://doc.qt.io/qt-6/qdatastream.html#Version-enum +#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)) + dataStream.setVersion( QDataStream::Qt_6_6 ); +#elif (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + dataStream.setVersion( QDataStream::Qt_6_0 ); +#else + dataStream.setVersion( QDataStream::QDataStream::Qt_5_15 ); +#endif + + dataStream << 0x00010002; // Magic number + dataStream << (quint32)0x00000001; // Protocol version + dataStream << static_cast( type ); // Message type + dataStream << instanceId; // Instance ID + dataStream << (qsizetype)content.size(); + dataStream.writeRawData( content.constData(), content.length() ); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + quint16 checksum = qChecksum( QByteArray( content.constData(), static_cast( content.length()))); +#else + quint16 checksum = qChecksum( content.constData(), static_cast( content.length())); +#endif + dataStream << checksum; + + return dataStream.status() == QDataStream::Ok; +} \ No newline at end of file diff --git a/message_coder.h b/message_coder.h new file mode 100644 index 0000000..e541c4b --- /dev/null +++ b/message_coder.h @@ -0,0 +1,62 @@ +// Copyright (c) Itay Grudev 2023 +// +// 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: +// +// Permission is not granted to use this software or any of the associated files +// as sample data for the purposes of building machine learning models. +// +// 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. + +#ifndef MESSAGE_CODER_H +#define MESSAGE_CODER_H + +#include +#include + +#include "singleapplication.h" + +class MessageCoder : public QObject { +Q_OBJECT +public: + /** + * @brief Constructs MessageCoder from a QLocalSocket + * @param message + */ + MessageCoder( QLocalSocket *socket ); + + /** + * @brief Send a MessageCoder on a QDataStream + * @param type + * @param instanceId + * @param content + */ + bool sendMessage( SingleApplication::MessageType type, quint16 instanceId, QByteArray content ); + +Q_SIGNALS: + void messageReceived( SingleApplication::Message message ); + +private Q_SLOTS: + void slotDataAvailable(); + + +private: + QLocalSocket *socket; + QDataStream dataStream; +}; + + +#endif // MESSAGE_CODER_H diff --git a/singleapplication.cpp b/singleapplication.cpp index 3e8fcb5..94fbac6 100644 --- a/singleapplication.cpp +++ b/singleapplication.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Itay Grudev 2015 - 2023 +// Copyright (c) Itay Grudev 2023 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,6 +24,9 @@ #include #include #include +#include + +#include #include "singleapplication.h" #include "singleapplication_p.h" @@ -42,12 +45,9 @@ SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSeconda { Q_D( SingleApplication ); -#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) - // On Android and iOS since the library is not supported fallback to - // standard QApplication behaviour by simply returning at this point. - qWarning() << "SingleApplication is not supported on Android and iOS systems."; - return; -#endif + // Keep track of the initialization time of SingleApplication + QElapsedTimer time; + time.start(); // Store the current mode of the program d->options = options; @@ -60,107 +60,35 @@ SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSeconda // block and QLocalServer d->genBlockServerName(); - // To mitigate QSharedMemory issues with large amount of processes - // attempting to attach at the same time - 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. - d->memory = new QSharedMemory( d->blockServerName ); - d->memory->attach(); - delete d->memory; -#endif - // Guarantee thread safe behaviour with a shared memory block. - d->memory = new QSharedMemory( d->blockServerName ); + while( time.elapsed() < timeout ){ + if( d->connectToPrimary( (timeout - time.elapsed()) * 2 / 3 )){ + if( ! allowSecondary ) // If we are operating in single instance mode - terminate the program + ::exit( EXIT_SUCCESS ); - // Create a shared memory block - if( d->memory->create( sizeof( InstancesInfo ) )){ - // Initialize the shared memory block - if( ! d->memory->lock() ){ - qCritical() << "SingleApplication: Unable to lock memory block after create."; - abortSafely(); - } - d->initializeMemoryBlock(); - } else { - if( d->memory->error() == QSharedMemory::AlreadyExists ){ - // Attempt to attach to the memory segment - 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(); - } + d->notifySecondaryStart( timeout ); + return; } else { - qCritical() << "SingleApplication: Unable to create block."; - abortSafely(); - } - } - - auto *inst = static_cast( d->memory->data() ); - QElapsedTimer time; - time.start(); - - // Make sure the shared memory block is initialised and in consistent state - while( true ){ - // If the shared memory block's checksum is valid continue - if( d->blockChecksum() == inst->checksum ) break; - - // If more than 5s have elapsed, assume the primary instance crashed and - // assume it's position - if( time.elapsed() > 5000 ){ - qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. 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 initialise faster - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory for random wait."; - qDebug() << d->memory->errorString(); - } - SingleApplicationPrivate::randomSleep(); - if( ! d->memory->lock() ){ - qCritical() << "SingleApplication: Unable to lock memory after random wait."; - abortSafely(); - } - } - - if( inst->primary == false ){ - d->startPrimary(); - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory after primary start."; - qDebug() << d->memory->errorString(); + // Report unexpected errors + switch( d->socket->error() ){ + case QLocalSocket::SocketAccessError: + case QLocalSocket::SocketResourceError: + case QLocalSocket::DatagramTooLargeError: + case QLocalSocket::UnsupportedSocketOperationError: + case QLocalSocket::OperationError: + case QLocalSocket::UnknownSocketError: + qCritical() << "SingleApplication:" << d->socket->errorString(); + qDebug() << "SingleApplication:" << "Falling back to primary instance"; + break; + default: + break; + } + // If No server is listening then this is a promoted to a primary instance. + if( d->startPrimary( timeout )) + return; } - return; } - // Check if another instance can be started - if( allowSecondary ){ - d->startSecondary(); - if( d->options & Mode::SecondaryNotification ){ - d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance ); - } - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory after secondary start."; - qDebug() << d->memory->errorString(); - } - return; - } - - if( ! d->memory->unlock() ){ - qDebug() << "SingleApplication: Unable to unlock memory at end of execution."; - qDebug() << d->memory->errorString(); - } - - d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); - - delete d; - - ::exit( EXIT_SUCCESS ); + qFatal( "SingleApplication: Did not manage to initialize within the allocated time1out." ); } SingleApplication::~SingleApplication() @@ -237,33 +165,20 @@ QString SingleApplication::currentUser() const * @param message The message to send. * @param timeout the maximum timeout in milliseconds for blocking functions. * @param sendMode mode of operation - * @return true if the message was sent successfuly, false otherwise. + * @return true if the message was received successfuly, false otherwise. */ -bool SingleApplication::sendMessage( const QByteArray &message, int timeout, SendMode sendMode ) +bool SingleApplication::sendMessage( const QByteArray &messageBody, int timeout ) { Q_D( SingleApplication ); // Nobody to connect to if( isPrimary() ) return false; - // Make sure the socket is connected - if( ! d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ) ) - return false; +// SingleApplicationMessage message( SingleApplicationMessage::NewInstance, 0, messageBody ); +// return d->sendApplicationMessage( message , timeout ); - return d->writeConfirmedMessage( timeout, message, sendMode ); -} -/** - * Cleans up the shared memory block and exits with a failure. - * This function halts program execution. - */ -void SingleApplication::abortSafely() -{ - Q_D( SingleApplication ); - - qCritical() << "SingleApplication: " << d->memory->error() << d->memory->errorString(); - delete d; - ::exit( EXIT_FAILURE ); + return d->sendApplicationMessage( SingleApplication::MessageType::InstanceMessage, messageBody, timeout ); } QStringList SingleApplication::userData() const diff --git a/singleapplication.h b/singleapplication.h index 17e3b25..5175010 100644 --- a/singleapplication.h +++ b/singleapplication.h @@ -1,4 +1,4 @@ -// Copyright (c) Itay Grudev 2015 - 2023 +// Copyright (c) Itay Grudev 2023 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -47,6 +47,20 @@ class SingleApplication : public QAPPLICATION_CLASS using app_t = QAPPLICATION_CLASS; public: + // If you change this enum, make sure to update read validation code in message_decoder.cpp + enum MessageType : quint8 { + Acknowledge, + NewInstance, + InstanceMessage, + }; + Q_ENUM( MessageType ) + + struct Message { + MessageType type; + quint16 instanceId; + QByteArray content; + }; + /** * @brief Mode of operation of `SingleApplication`. * Whether the block should be user-wide or system-wide and whether the @@ -139,14 +153,6 @@ class SingleApplication : public QAPPLICATION_CLASS */ QString currentUser() const; - /** - * @brief Mode of operation of sendMessage. - */ - enum SendMode { - NonBlocking, /** Do not wait for the primary instance termination and return immediately */ - BlockUntilPrimaryExit, /** Wait until the primary instance is terminated */ - }; - /** * @brief Sends a message to the primary instance * @param message data to send @@ -155,7 +161,7 @@ class SingleApplication : public QAPPLICATION_CLASS * @returns `true` on success * @note sendMessage() will return false if invoked from the primary instance */ - bool sendMessage( const QByteArray &message, int timeout = 100, SendMode sendMode = NonBlocking ); + bool sendMessage( const QByteArray &message, int timeout = 100 ); /** * @brief Get the set user data. @@ -178,7 +184,6 @@ class SingleApplication : public QAPPLICATION_CLASS private: SingleApplicationPrivate *d_ptr; Q_DECLARE_PRIVATE(SingleApplication) - void abortSafely(); }; Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options) diff --git a/singleapplication.pri b/singleapplication.pri index ae81f59..9b2292b 100644 --- a/singleapplication.pri +++ b/singleapplication.pri @@ -3,9 +3,11 @@ CONFIG += c++11 HEADERS += $$PWD/SingleApplication \ $$PWD/singleapplication.h \ - $$PWD/singleapplication_p.h + $$PWD/singleapplication_p.h \ + $$PWD/singleapplicationmessage.h SOURCES += $$PWD/singleapplication.cpp \ - $$PWD/singleapplication_p.cpp + $$PWD/singleapplication_p.cpp \ + $$PWD/singleapplicationmessage.cpp INCLUDEPATH += $$PWD diff --git a/singleapplication_p.cpp b/singleapplication_p.cpp index 5499203..085da38 100644 --- a/singleapplication_p.cpp +++ b/singleapplication_p.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Itay Grudev 2015 - 2023 +// Copyright (c) Itay Grudev 2023 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -42,6 +42,8 @@ #include #include +#include "message_coder.h" + #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) #include #else @@ -66,12 +68,8 @@ #endif SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr ) - : q_ptr( q_ptr ) + : q_ptr( q_ptr ), server( nullptr ), socket( nullptr ), instanceNumber( 0 ), connectionMap() { - server = nullptr; - socket = nullptr; - memory = nullptr; - instanceNumber = 0; } SingleApplicationPrivate::~SingleApplicationPrivate() @@ -80,22 +78,6 @@ SingleApplicationPrivate::~SingleApplicationPrivate() socket->close(); delete socket; } - - if( memory != nullptr ){ - memory->lock(); - auto *inst = static_cast(memory->data()); - if( server != nullptr ){ - server->close(); - delete server; - inst->primary = false; - inst->primaryPid = -1; - inst->primaryUser[0] = '\0'; - inst->checksum = blockChecksum(); - } - memory->unlock(); - - delete memory; - } } QString SingleApplicationPrivate::getUsername() @@ -135,7 +117,7 @@ void SingleApplicationPrivate::genBlockServerName() #if QT_VERSION < QT_VERSION_CHECK(6, 3, 0) appData.addData( "SingleApplication", 17 ); #else - appData.addData( QByteArrayView{"SingleApplication"} ); + appData.addData( QByteArrayView{"SingleApplication"} ); #endif appData.addData( SingleApplication::app_t::applicationName().toUtf8() ); appData.addData( SingleApplication::app_t::organizationName().toUtf8() ); @@ -175,26 +157,8 @@ void SingleApplicationPrivate::genBlockServerName() blockServerName = QString::fromUtf8(appData.result().toBase64().replace("/", "_")); } -void SingleApplicationPrivate::initializeMemoryBlock() const -{ - auto *inst = static_cast( memory->data() ); - inst->primary = false; - inst->secondary = 0; - inst->primaryPid = -1; - inst->primaryUser[0] = '\0'; - inst->checksum = blockChecksum(); -} - -void SingleApplicationPrivate::startPrimary() +bool SingleApplicationPrivate::startPrimary( uint timeout ) { - // Reset the number of connections - auto *inst = static_cast ( memory->data() ); - - inst->primary = true; - inst->primaryPid = QCoreApplication::applicationPid(); - qstrncpy( inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser) ); - inst->checksum = blockChecksum(); - instanceNumber = 0; // Successful creation means that no main process exists // So we start a QLocalServer to listen for connections QLocalServer::removeServer( blockServerName ); @@ -208,141 +172,88 @@ void SingleApplicationPrivate::startPrimary() server->setSocketOptions( QLocalServer::WorldAccessOption ); } - server->listen( blockServerName ); QObject::connect( server, &QLocalServer::newConnection, this, &SingleApplicationPrivate::slotConnectionEstablished ); -} -void SingleApplicationPrivate::startSecondary() -{ - auto *inst = static_cast ( memory->data() ); + if( server->listen( blockServerName ) ) + return true; - inst->secondary += 1; - inst->checksum = blockChecksum(); - instanceNumber = inst->secondary; + delete server; + return false; } -bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType ) -{ - QElapsedTimer time; - time.start(); - - // Connect to the Local Server of the Primary Instance if not already - // connected. - if( socket == nullptr ){ - socket = new QLocalSocket(); - } - - if( socket->state() == QLocalSocket::ConnectedState ) return true; - - if( socket->state() != QLocalSocket::ConnectedState ){ - - while( true ){ - randomSleep(); - - if( socket->state() != QLocalSocket::ConnectingState ) - socket->connectToServer( blockServerName ); - - if( socket->state() == QLocalSocket::ConnectingState ){ - socket->waitForConnected( static_cast(msecs - time.elapsed()) ); - } - - // If connected break out of the loop - if( socket->state() == QLocalSocket::ConnectedState ) break; +bool SingleApplicationPrivate::connectToPrimary( uint timeout ){ + if( socket == nullptr ) + socket = new QLocalSocket( this ); - // If elapsed time since start is longer than the method timeout return - if( time.elapsed() >= msecs ) return false; - } - } - - // Initialisation message according to the SingleApplication protocol - QByteArray initMsg; - QDataStream writeStream(&initMsg, QIODevice::WriteOnly); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - writeStream.setVersion(QDataStream::Qt_5_6); -#endif - - writeStream << blockServerName.toLatin1(); - writeStream << static_cast(connectionType); - writeStream << instanceNumber; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - quint16 checksum = qChecksum(QByteArray(initMsg.constData(), static_cast(initMsg.length()))); -#else - quint16 checksum = qChecksum(initMsg.constData(), static_cast(initMsg.length())); -#endif - writeStream << checksum; + if( socket->state() == QLocalSocket::ConnectedState ) + return true; - return writeConfirmedMessage( static_cast(msecs - time.elapsed()), initMsg ); -} + if( socket->state() != QLocalSocket::ConnectingState ) + socket->connectToServer( blockServerName ); -void SingleApplicationPrivate::writeAck( QLocalSocket *sock ) { - sock->putChar('\n'); + return socket->waitForConnected( timeout ); } -bool SingleApplicationPrivate::writeConfirmedMessage (int msecs, const QByteArray &msg, SingleApplication::SendMode sendMode) +void SingleApplicationPrivate::notifySecondaryStart( uint timeout ) { - QElapsedTimer time; - time.start(); +// SingleApplicationMessage message( SingleApplicationMessage::NewInstance, 0, QByteArray() ); +// sendApplicationMessage( message, timeout ); + sendApplicationMessage( SingleApplication::MessageType::NewInstance, QByteArray(), timeout ); +} - // Frame 1: The header indicates the message length that follows - QByteArray header; - QDataStream headerStream(&header, QIODevice::WriteOnly); +//bool SingleApplicationPrivate::sendApplicationMessage( SingleApplicationMessage message, uint timeout ) +//{ +// SingleApplicationMessage response; +// return sendApplicationMessage( message, timeout, response ); +//} -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion(QDataStream::Qt_5_6); -#endif - headerStream << static_cast ( msg.length() ); +bool SingleApplicationPrivate::sendApplicationMessage( SingleApplication::MessageType messageType, QByteArray content, uint timeout ) +{ + QElapsedTimer elapsedTime; + elapsedTime.start(); - if( ! writeConfirmedFrame( static_cast(msecs - time.elapsed()), header )) + if( ! connectToPrimary( timeout * 2 / 3 )) return false; - // Frame 2: The message - const bool result = writeConfirmedFrame( static_cast(msecs - time.elapsed()), msg ); - - // Block if needed - if (socket && sendMode == SingleApplication::BlockUntilPrimaryExit) - socket->waitForDisconnected(-1); + MessageCoder coder( socket ); + coder.sendMessage( messageType, instanceNumber, content ); - return result; -} - -bool SingleApplicationPrivate::writeConfirmedFrame( int msecs, const QByteArray &msg ) -{ - socket->write( msg ); socket->flush(); + return socket->waitForBytesWritten( qMax(timeout - elapsedTime.elapsed(), 1) ); - bool result = socket->waitForReadyRead( msecs ); // await ack byte - if (result) { - socket->read( 1 ); - return true; - } - - return false; -} - -quint16 SingleApplicationPrivate::blockChecksum() const -{ -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - quint16 checksum = qChecksum(QByteArray(static_cast(memory->constData()), offsetof(InstancesInfo, checksum))); -#else - quint16 checksum = qChecksum(static_cast(memory->constData()), offsetof(InstancesInfo, checksum)); -#endif - return checksum; + // TODO: Wait for an ACK message +// if( socket->waitForReadyRead( timeout )){ +// QByteArray responseBytes = socket->readAll(); +// response = SingleApplicationMessage( socket->readAll() ); +// +// // The response message is invalid +// if( response.invalid ) +// return false; +// +// // The response message didn't contain the primary instance id +// if( response.instanceId != 0 ) +// return false; +// +// // This isn't an acknowledge message +// if( response.type != SingleApplicationMessage::Acknowledge ) +// return false; +// +// return true; +// } +// +// return false; } qint64 SingleApplicationPrivate::primaryPid() const { qint64 pid; - memory->lock(); - auto *inst = static_cast( memory->data() ); - pid = inst->primaryPid; - memory->unlock(); + // TODO: Reimplement with message response return pid; } @@ -351,10 +262,7 @@ QString SingleApplicationPrivate::primaryUser() const { QByteArray username; - memory->lock(); - auto *inst = static_cast( memory->data() ); - username = inst->primaryUser; - memory->unlock(); + // TODO: Reimplement with message response return QString::fromUtf8( username ); } @@ -365,169 +273,17 @@ QString SingleApplicationPrivate::primaryUser() const void SingleApplicationPrivate::slotConnectionEstablished() { QLocalSocket *nextConnSocket = server->nextPendingConnection(); - connectionMap.insert(nextConnSocket, ConnectionInfo()); - QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, - [nextConnSocket, this](){ - auto &info = connectionMap[nextConnSocket]; - this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); - } - ); + connectionMap.insert( nextConnSocket, ConnectionInfo() ); + connectionMap[nextConnSocket].coder = new MessageCoder( nextConnSocket ); - QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater); + QObject::connect( nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater ); - QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, + QObject::connect( nextConnSocket, &QLocalSocket::destroyed, this, [nextConnSocket, this](){ - connectionMap.remove(nextConnSocket); + connectionMap.remove( nextConnSocket ); } ); - - QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, - [nextConnSocket, this](){ - auto &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: - this->slotDataAvailable( nextConnSocket, info.instanceId ); - break; - default: - break; - }; - } - ); -} - -void SingleApplicationPrivate::readMessageHeader( QLocalSocket *sock, SingleApplicationPrivate::ConnectionStage nextStage ) -{ - if (!connectionMap.contains( sock )){ - return; - } - - if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){ - return; - } - - QDataStream headerStream( sock ); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion( QDataStream::Qt_5_6 ); -#endif - - // 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 SingleApplicationPrivate::isFrameComplete( QLocalSocket *sock ) -{ - if (!connectionMap.contains( sock )){ - return false; - } - - ConnectionInfo &info = connectionMap[sock]; - if( sock->bytesAvailable() < ( qint64 )info.msgLen ){ - return false; - } - - return true; -} - -void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) -{ - Q_Q(SingleApplication); - - if( !isFrameComplete( sock ) ) - return; - - // Read the message body - QByteArray msgBytes = sock->readAll(); - QDataStream readStream(msgBytes); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - readStream.setVersion( QDataStream::Qt_5_6 ); -#endif - - // server name - QByteArray latin1Name; - readStream >> latin1Name; - - // connection type - ConnectionType connectionType = InvalidConnection; - quint8 connTypeVal = InvalidConnection; - readStream >> connTypeVal; - connectionType = static_cast ( 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.constData(), static_cast(msgBytes.length() - sizeof(quint16)))); -#else - const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast(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 & SingleApplication::Mode::SecondaryNotification ) ) - { - Q_EMIT q->instanceStarted(); - } - - writeAck( sock ); -} - -void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId ) -{ - Q_Q(SingleApplication); - - if ( !isFrameComplete( dataSocket ) ) - return; - - const QByteArray message = dataSocket->readAll(); - - writeAck( dataSocket ); - - ConnectionInfo &info = connectionMap[dataSocket]; - info.stage = StageConnectedHeader; - - Q_EMIT q->receivedMessage( instanceId, message); -} - -void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId ) -{ - if( closedSocket->bytesAvailable() > 0 ) - slotDataAvailable( closedSocket, instanceId ); } void SingleApplicationPrivate::randomSleep() diff --git a/singleapplication_p.h b/singleapplication_p.h index 6b21516..d5ad1e5 100644 --- a/singleapplication_p.h +++ b/singleapplication_p.h @@ -1,4 +1,4 @@ -// Copyright (c) Itay Grudev 2015 - 2023 +// Copyright (c) Itay Grudev 2023 // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -36,7 +36,10 @@ #include #include #include + #include "singleapplication.h" +#include "message_coder.h" +#include "singleapplicationmessage.h" struct InstancesInfo { bool primary; @@ -47,20 +50,13 @@ struct InstancesInfo { }; struct ConnectionInfo { - qint64 msgLen = 0; quint32 instanceId = 0; - quint8 stage = 0; + MessageCoder *coder; }; class SingleApplicationPrivate : public QObject { Q_OBJECT public: - enum ConnectionType : quint8 { - InvalidConnection = 0, - NewInstance = 1, - SecondaryInstance = 2, - Reconnect = 3 - }; enum ConnectionStage : quint8 { StageInitHeader = 0, StageInitBody = 1, @@ -75,24 +71,22 @@ Q_OBJECT static QString getUsername(); void genBlockServerName(); void initializeMemoryBlock() const; - void startPrimary(); - void startSecondary(); - bool connectToPrimary( int msecs, ConnectionType connectionType ); + bool connectToPrimary( uint timeout ); + bool startPrimary( uint timeout ); + void notifySecondaryStart( uint timeout ); + bool sendApplicationMessage( SingleApplication::MessageType messageType, QByteArray content, uint timeout ); +// bool sendApplicationMessage( SingleApplication::MessageType messageType, QByteArray content, uint timeout, SingleApplicationMessage &response ); quint16 blockChecksum() const; qint64 primaryPid() const; QString primaryUser() const; bool isFrameComplete(QLocalSocket *sock); void readMessageHeader(QLocalSocket *socket, ConnectionStage nextStage); void readInitMessageBody(QLocalSocket *socket); - void writeAck(QLocalSocket *sock); - bool writeConfirmedFrame(int msecs, const QByteArray &msg); - bool writeConfirmedMessage(int msecs, const QByteArray &msg, SingleApplication::SendMode sendMode = SingleApplication::NonBlocking); static void randomSleep(); void addAppData(const QString &data); QStringList appData() const; SingleApplication *q_ptr; - QSharedMemory *memory; QLocalSocket *socket; QLocalServer *server; quint32 instanceNumber; @@ -103,8 +97,6 @@ Q_OBJECT public Q_SLOTS: void slotConnectionEstablished(); - void slotDataAvailable( QLocalSocket*, quint32 ); - void slotClientConnectionClosed( QLocalSocket*, quint32 ); }; #endif // SINGLEAPPLICATION_P_H diff --git a/singleapplicationmessage.cpp b/singleapplicationmessage.cpp index 5b6c750..09eaa63 100644 --- a/singleapplicationmessage.cpp +++ b/singleapplicationmessage.cpp @@ -53,9 +53,9 @@ SingleApplicationMessage::SingleApplicationMessage( QByteArray message ) dataStream >> messageChecksum; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - const quint16 computedChecksum = qChecksum(QByteArray(message.constData(), static_cast(message.length() - sizeof(quint16)))); + const quint16 computedChecksum = qChecksum( QByteArray(message.constData(), static_cast( message.length() - sizeof(quint16) ))); #else - const quint16 computedChecksum = qChecksum(message.constData(), static_cast(message.length() - sizeof(quint16))); + const quint16 computedChecksum = qChecksum( message.constData(), static_cast( message.length() - sizeof(quint16) )); #endif if( messageChecksum != computedChecksum ) @@ -76,9 +76,9 @@ SingleApplicationMessage:: operator QByteArray() dataStream << (qsizetype)content.size(); dataStream << content; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - quint16 checksum = qChecksum( QByteArray( message.constData(), static_cast( message.length()))); + quint16 checksum = qChecksum( QByteArray( message.constData(), static_cast( message.length() ))); #else - quint16 checksum = qChecksum( message.constData(), static_cast( messageMsg.length())); + quint16 checksum = qChecksum( message.constData(), static_cast( messageMsg.length() )); #endif dataStream << checksum;