Skip to content

Commit

Permalink
WIP implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
itay-grudev committed May 20, 2024
1 parent 7b4e72d commit d15fa8f
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 465 deletions.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -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.
235 changes: 235 additions & 0 deletions message_coder.cpp
Original file line number Diff line number Diff line change
@@ -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 <QDebug>
#include <QIODevice>

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<quint32>(msg.content.length())));
#else
const quint16 computedChecksum = qChecksum(msg.content.constData(), static_cast<quint32>(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<quint8>( 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<quint32>( content.length())));
#else
quint16 checksum = qChecksum( content.constData(), static_cast<quint32>( content.length()));
#endif
dataStream << checksum;

return dataStream.status() == QDataStream::Ok;
}
62 changes: 62 additions & 0 deletions message_coder.h
Original file line number Diff line number Diff line change
@@ -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 <QByteArray>
#include <QException>

#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
Loading

0 comments on commit d15fa8f

Please sign in to comment.