diff --git a/SingleApplication/CHANGELOG.md b/SingleApplication/CHANGELOG.md index 45c197e3d..e2ba290e2 100644 --- a/SingleApplication/CHANGELOG.md +++ b/SingleApplication/CHANGELOG.md @@ -1,12 +1,79 @@ Changelog ========= +If by accident I have forgotten to credit someone in the CHANGELOG, email me and I will fix it. + +__3.2.0__ +--------- + +* Added support for Qt 6 - _Jonas Kvinge_ +* Fixed warning in `Qt 5.9` with `min`/`max` functions on Windows - _Nick Korotysh_ +* Fix return value of connectToPrimary() when connect is successful - _Jonas Kvinge_ +* Fix build issue with MinGW GCC pedantic mode - _Iakov Kirilenko_ +* Fixed conversion from `int` to `quint32` and Clang Tidy warnings - _Hennadii Chernyshchyk_ + +__3.1.5__ +--------- + +* Improved library stability in edge cases and very rapid process initialisation +* Fixed Bug where the shared memory block may have been modified without a lock +* Fixed Bug causing `instanceStarted()` to not get emitted when a second instance + has been started before the primary has initiated it's `QLocalServer`. + +__3.1.4__ +--------- +* Officially supporting and build-testing against Qt 5.15 +* Fixed an MSVC C4996 warning that suggests using `strncpy_s`. + + _Hennadii Chernyshchyk_ + +__3.1.3.1__ +--------- +* CMake build system improvements +* Fixed Clang Tidy warnings + + _Hennadii Chernyshchyk_ + +__3.1.3__ +--------- +* Improved `CMakeLists.txt` + + _Hennadii Chernyshchyk_ + +__3.1.2__ +--------- + +* Fix a crash when exiting an application on Android and iOS + + _Emeric Grange_ + +__3.1.1a__ +---------- + +* Added currentUser() method that returns the user the current instance is running as. + + _Leander Schulten_ + +__3.1.0a__ +---------- + +* Added primaryUser() method that returns the user the primary instance is running as. + +__3.0.19__ +---------- + +* Fixed code warning for depricated functions in Qt 5.10 related to `QTime` and `qrand()`. + + _Hennadii Chernyshchyk_ + _Anton Filimonov_ + _Jonas Kvinge_ + __3.0.18__ ---------- * Fallback to standard QApplication class on iOS and Android systems where the library is not supported. - + * Added Build CI tests to verify the library builds successfully on Linux, Windows and MacOS across multiple Qt versions. _Anton Filimonov_ diff --git a/SingleApplication/CMakeLists.txt b/SingleApplication/CMakeLists.txt index d61923043..ae1b1439b 100644 --- a/SingleApplication/CMakeLists.txt +++ b/SingleApplication/CMakeLists.txt @@ -1,43 +1,40 @@ -cmake_minimum_required(VERSION 3.1.0) +cmake_minimum_required(VERSION 3.7.0) -project(SingleApplication) +project(SingleApplication LANGUAGES CXX) -set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) -# SingleApplication base class -set(QAPPLICATION_CLASS QCoreApplication CACHE STRING "Inheritance class for SingleApplication") -set_property(CACHE QAPPLICATION_CLASS PROPERTY STRINGS QApplication QGuiApplication QCoreApplication) - -# Libary target add_library(${PROJECT_NAME} STATIC singleapplication.cpp singleapplication_p.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") +endif() # Find dependencies -find_package(Qt5Network) -if(QAPPLICATION_CLASS STREQUAL QApplication) - find_package(Qt5 COMPONENTS Widgets REQUIRED) -elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication) - find_package(Qt5 COMPONENTS Gui REQUIRED) -else() - find_package(Qt5 COMPONENTS Core REQUIRED) -endif() -add_compile_definitions(QAPPLICATION_CLASS=${QAPPLICATION_CLASS}) +set(QT_COMPONENTS Core Network) +set(QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Core Qt${QT_DEFAULT_MAJOR_VERSION}::Network) -# Link dependencies -target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Network) if(QAPPLICATION_CLASS STREQUAL QApplication) - target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Widgets) + list(APPEND QT_COMPONENTS Widgets) + list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Widgets) elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication) - target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Gui) + list(APPEND QT_COMPONENTS Gui) + list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Gui) else() - target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Core) + set(QAPPLICATION_CLASS QCoreApplication) endif() +find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED) + +target_link_libraries(${PROJECT_NAME} PUBLIC ${QT_LIBRARIES}) + if(WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE advapi32) endif() +target_compile_definitions(${PROJECT_NAME} PUBLIC QAPPLICATION_CLASS=${QAPPLICATION_CLASS}) target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/SingleApplication/LICENSE b/SingleApplication/LICENSE index 85b2a1495..a82e5a68f 100644 --- a/SingleApplication/LICENSE +++ b/SingleApplication/LICENSE @@ -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 diff --git a/SingleApplication/README.md b/SingleApplication/README.md index 5d6098654..457ab3392 100644 --- a/SingleApplication/README.md +++ b/SingleApplication/README.md @@ -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,8 +32,7 @@ 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 @@ -66,13 +54,27 @@ 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 @@ -137,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 @@ -152,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.* @@ -204,6 +215,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 diff --git a/SingleApplication/SingleApplication b/SingleApplication/SingleApplication new file mode 100644 index 000000000..8ead1a428 --- /dev/null +++ b/SingleApplication/SingleApplication @@ -0,0 +1 @@ +#include "singleapplication.h" diff --git a/SingleApplication/examples/basic/CMakeLists.txt b/SingleApplication/examples/basic/CMakeLists.txt new file mode 100644 index 000000000..c1429230c --- /dev/null +++ b/SingleApplication/examples/basic/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.7.0) + +project(basic LANGUAGES CXX) + +# SingleApplication base class +set(QAPPLICATION_CLASS QCoreApplication) +add_subdirectory(../.. SingleApplication) + +add_executable(basic main.cpp) + +target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication) + diff --git a/SingleApplication/examples/basic/basic.pro b/SingleApplication/examples/basic/basic.pro old mode 100644 new mode 100755 diff --git a/SingleApplication/examples/basic/main.cpp b/SingleApplication/examples/basic/main.cpp old mode 100644 new mode 100755 index 1841902be..c22af037c --- a/SingleApplication/examples/basic/main.cpp +++ b/SingleApplication/examples/basic/main.cpp @@ -5,5 +5,7 @@ int main(int argc, char *argv[]) // Allow secondary instances SingleApplication app( argc, argv ); + qWarning() << "Started a new instance"; + return app.exec(); } diff --git a/SingleApplication/examples/calculator/CMakeLists.txt b/SingleApplication/examples/calculator/CMakeLists.txt new file mode 100644 index 000000000..82305f047 --- /dev/null +++ b/SingleApplication/examples/calculator/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.7.0) + +project(calculator LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +# SingleApplication base class +set(QAPPLICATION_CLASS QApplication) +add_subdirectory(../.. SingleApplication) + +find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED) + +add_executable(${PROJECT_NAME} + button.h + calculator.h + button.cpp + calculator.cpp + main.cpp +) + +target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication) diff --git a/SingleApplication/examples/calculator/calculator.cpp b/SingleApplication/examples/calculator/calculator.cpp index c02b5604a..3d34c2a74 100644 --- a/SingleApplication/examples/calculator/calculator.cpp +++ b/SingleApplication/examples/calculator/calculator.cpp @@ -82,27 +82,27 @@ Calculator::Calculator(QWidget *parent) digitButtons[i] = createButton(QString::number(i), SLOT(digitClicked())); } - Button *pointButton = createButton(tr("."), SLOT(pointClicked())); - Button *changeSignButton = createButton(tr("\302\261"), SLOT(changeSignClicked())); + Button *pointButton = createButton(".", SLOT(pointClicked())); + Button *changeSignButton = createButton("\302\261", SLOT(changeSignClicked())); - Button *backspaceButton = createButton(tr("Backspace"), SLOT(backspaceClicked())); - Button *clearButton = createButton(tr("Clear"), SLOT(clear())); - Button *clearAllButton = createButton(tr("Clear All"), SLOT(clearAll())); + Button *backspaceButton = createButton("Backspace", SLOT(backspaceClicked())); + Button *clearButton = createButton("Clear", SLOT(clear())); + Button *clearAllButton = createButton("Clear All", SLOT(clearAll())); - Button *clearMemoryButton = createButton(tr("MC"), SLOT(clearMemory())); - Button *readMemoryButton = createButton(tr("MR"), SLOT(readMemory())); - Button *setMemoryButton = createButton(tr("MS"), SLOT(setMemory())); - Button *addToMemoryButton = createButton(tr("M+"), SLOT(addToMemory())); + Button *clearMemoryButton = createButton("MC", SLOT(clearMemory())); + Button *readMemoryButton = createButton("MR", SLOT(readMemory())); + Button *setMemoryButton = createButton("MS", SLOT(setMemory())); + Button *addToMemoryButton = createButton("M+", SLOT(addToMemory())); - Button *divisionButton = createButton(tr("\303\267"), SLOT(multiplicativeOperatorClicked())); - Button *timesButton = createButton(tr("\303\227"), SLOT(multiplicativeOperatorClicked())); - Button *minusButton = createButton(tr("-"), SLOT(additiveOperatorClicked())); - Button *plusButton = createButton(tr("+"), SLOT(additiveOperatorClicked())); + Button *divisionButton = createButton("\303\267", SLOT(multiplicativeOperatorClicked())); + Button *timesButton = createButton("\303\227", SLOT(multiplicativeOperatorClicked())); + Button *minusButton = createButton("-", SLOT(additiveOperatorClicked())); + Button *plusButton = createButton("+", SLOT(additiveOperatorClicked())); - Button *squareRootButton = createButton(tr("Sqrt"), SLOT(unaryOperatorClicked())); - Button *powerButton = createButton(tr("x\302\262"), SLOT(unaryOperatorClicked())); - Button *reciprocalButton = createButton(tr("1/x"), SLOT(unaryOperatorClicked())); - Button *equalButton = createButton(tr("="), SLOT(equalClicked())); + Button *squareRootButton = createButton("Sqrt", SLOT(unaryOperatorClicked())); + Button *powerButton = createButton("x\302\262", SLOT(unaryOperatorClicked())); + Button *reciprocalButton = createButton("1/x", SLOT(unaryOperatorClicked())); + Button *equalButton = createButton("=", SLOT(equalClicked())); //! [4] //! [5] @@ -140,7 +140,7 @@ Calculator::Calculator(QWidget *parent) mainLayout->addWidget(equalButton, 5, 5); setLayout(mainLayout); - setWindowTitle(tr("Calculator")); + setWindowTitle("Calculator"); } //! [6] @@ -169,15 +169,15 @@ void Calculator::unaryOperatorClicked() double operand = display->text().toDouble(); double result = 0.0; - if (clickedOperator == tr("Sqrt")) { + if (clickedOperator == "Sqrt") { if (operand < 0.0) { abortOperation(); return; } result = std::sqrt(operand); - } else if (clickedOperator == tr("x\302\262")) { + } else if (clickedOperator == "x\302\262") { result = std::pow(operand, 2.0); - } else if (clickedOperator == tr("1/x")) { + } else if (clickedOperator == "1/x") { if (operand == 0.0) { abortOperation(); return; @@ -287,7 +287,7 @@ void Calculator::pointClicked() if (waitingForOperand) display->setText("0"); if (!display->text().contains('.')) - display->setText(display->text() + tr(".")); + display->setText(display->text() + "."); waitingForOperand = false; } //! [22] @@ -299,7 +299,7 @@ void Calculator::changeSignClicked() double value = text.toDouble(); if (value > 0.0) { - text.prepend(tr("-")); + text.prepend("-"); } else if (value < 0.0) { text.remove(0, 1); } @@ -383,20 +383,20 @@ Button *Calculator::createButton(const QString &text, const char *member) void Calculator::abortOperation() { clearAll(); - display->setText(tr("####")); + display->setText("####"); } //! [36] //! [38] bool Calculator::calculate(double rightOperand, const QString &pendingOperator) { - if (pendingOperator == tr("+")) { + if (pendingOperator == "+") { sumSoFar += rightOperand; - } else if (pendingOperator == tr("-")) { + } else if (pendingOperator == "-") { sumSoFar -= rightOperand; - } else if (pendingOperator == tr("\303\227")) { + } else if (pendingOperator == "\303\227") { factorSoFar *= rightOperand; - } else if (pendingOperator == tr("\303\267")) { + } else if (pendingOperator == "\303\267") { if (rightOperand == 0.0) return false; factorSoFar /= rightOperand; diff --git a/SingleApplication/examples/sending_arguments/CMakeLists.txt b/SingleApplication/examples/sending_arguments/CMakeLists.txt new file mode 100644 index 000000000..2cc559757 --- /dev/null +++ b/SingleApplication/examples/sending_arguments/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.7.0) + +project(sending_arguments LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +# SingleApplication base class +set(QAPPLICATION_CLASS QCoreApplication) +add_subdirectory(../.. SingleApplication) + +find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED) + +add_executable(${PROJECT_NAME} + main.cpp + messagereceiver.cpp + messagereceiver.h + main.cpp +) + +target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication) diff --git a/SingleApplication/examples/sending_arguments/main.cpp b/SingleApplication/examples/sending_arguments/main.cpp old mode 100644 new mode 100755 index d90b250ba..a9d34dd97 --- a/SingleApplication/examples/sending_arguments/main.cpp +++ b/SingleApplication/examples/sending_arguments/main.cpp @@ -11,6 +11,9 @@ int main(int argc, char *argv[]) // If this is a secondary instance if( app.isSecondary() ) { app.sendMessage( app.arguments().join(' ').toUtf8() ); + qDebug() << "App already running."; + qDebug() << "Primary instance PID: " << app.primaryPid(); + qDebug() << "Primary instance user: " << app.primaryUser(); return 0; } else { QObject::connect( diff --git a/SingleApplication/examples/sending_arguments/sending_arguments.pro b/SingleApplication/examples/sending_arguments/sending_arguments.pro old mode 100644 new mode 100755 diff --git a/SingleApplication/singleapplication.cpp b/SingleApplication/singleapplication.cpp index 461592ebe..7e153a004 100644 --- a/SingleApplication/singleapplication.cpp +++ b/SingleApplication/singleapplication.cpp @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) Itay Grudev 2015 - 2018 +// 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 @@ -20,176 +20,255 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#include -#include -#include +#include #include #include -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) // ### Qt 6: remove -#else -#if TODO_LIST -#pragma message("@TODO remove code for QT 5.10 or later") -#endif -#include -#endif + #include "singleapplication.h" #include "singleapplication_p.h" /** - @brief Constructor. Checks and fires up LocalServer or closes the program - if another instance already exists - @param argc - @param argv - @param {bool} allowSecondaryInstances -*/ -SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout ) - : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) + * @brief Constructor. Checks and fires up LocalServer or closes the program + * if another instance already exists + * @param argc + * @param argv + * @param allowSecondary Whether to enable secondary instance support + * @param options Optional flags to toggle specific behaviour + * @param timeout Maximum time blocking functions are allowed during app load + */ +SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, const QString &userData ) + : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) { - Q_D(SingleApplication); + 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; + // 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 - // Store the current mode of the program - d->options = options; + // Store the current mode of the program + d->options = options; - // Generating an application ID used for identifying the shared memory - // block and QLocalServer - d->genBlockServerName(); + // Add any unique user data + if ( ! userData.isEmpty() ) + d->addAppData( userData ); + + // Generating an application ID used for identifying the shared memory + // block and QLocalServer + d->genBlockServerName(); + + // To mitigate QSharedMemory issues with large amount of processes + // attempting to attach at the same time + 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; + // 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 ); + // Guarantee thread safe behaviour with a shared memory block. + d->memory = new QSharedMemory( d->blockServerName ); - // Create a shared memory block - if( d->memory->create( sizeof( InstancesInfo ) ) ) { - // Initialize the shared memory block - d->memory->lock(); - d->initializeMemoryBlock(); - d->memory->unlock(); - } else { - // Attempt to attach to the memory segment - if( ! d->memory->attach() ) { - qCritical() << "SingleApplication: Unable to attach to shared memory block."; - qCritical() << d->memory->errorString(); - delete d; - ::exit( EXIT_FAILURE ); - } - } + // 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(); + } + } else { + qCritical() << "SingleApplication: Unable to create block."; + abortSafely(); + } + } - InstancesInfo* inst = static_cast( d->memory->data() ); - QElapsedTimer time; - time.start(); + 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 ) { - d->memory->lock(); + // 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( 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(); + } - 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(); + } + } - d->memory->unlock(); + if( inst->primary == false ){ + d->startPrimary(); + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory after primary start."; + qDebug() << d->memory->errorString(); + } + return; + } - // Random sleep here limits the probability of a collision between two racing apps -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) // ### Qt 6: remove - qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits::max() ); - QThread::sleep( 8 + static_cast ( static_cast ( qrand() ) / RAND_MAX * 10 ) ); -#else -#if TODO_LIST -#pragma message("@TODO remove code for QT 5.10 or later") -#endif - quint32 value = QRandomGenerator::global()->generate(); - QThread::sleep(8 + static_cast(value / RAND_MAX * 10)); -#endif - } + // 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( inst->primary == false) { - d->startPrimary(); - d->memory->unlock(); - return; - } + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory at end of execution."; + qDebug() << d->memory->errorString(); + } - // Check if another instance can be started - if( allowSecondary ) { - inst->secondary += 1; - inst->checksum = d->blockChecksum(); - d->instanceNumber = inst->secondary; - d->startSecondary(); - if( d->options & Mode::SecondaryNotification ) { - d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance ); - } - d->memory->unlock(); - return; - } + d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); - d->memory->unlock(); + delete d; - d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); + ::exit( EXIT_SUCCESS ); +} - delete d; - - ::exit( EXIT_SUCCESS ); +SingleApplication::~SingleApplication() +{ + Q_D( SingleApplication ); + delete d; } /** - @brief Destructor -*/ -SingleApplication::~SingleApplication() + * Checks if the current application instance is primary. + * @return Returns true if the instance is primary, false otherwise. + */ +bool SingleApplication::isPrimary() const { - Q_D(SingleApplication); - delete d; + Q_D( const SingleApplication ); + return d->server != nullptr; } -bool SingleApplication::isPrimary() +/** + * Checks if the current application instance is secondary. + * @return Returns true if the instance is secondary, false otherwise. + */ +bool SingleApplication::isSecondary() const { - Q_D(SingleApplication); - return d->server != nullptr; + Q_D( const SingleApplication ); + return d->server == nullptr; } -bool SingleApplication::isSecondary() +/** + * Allows you to identify an instance by returning unique consecutive instance + * ids. It is reset when the first (primary) instance of your app starts and + * only incremented afterwards. + * @return Returns a unique instance id. + */ +quint32 SingleApplication::instanceId() const { - Q_D(SingleApplication); - return d->server == nullptr; + Q_D( const SingleApplication ); + return d->instanceNumber; } -quint32 SingleApplication::instanceId() +/** + * Returns the OS PID (Process Identifier) of the process running the primary + * instance. Especially useful when SingleApplication is coupled with OS. + * specific APIs. + * @return Returns the primary instance PID. + */ +qint64 SingleApplication::primaryPid() const { - Q_D(SingleApplication); - return d->instanceNumber; + Q_D( const SingleApplication ); + return d->primaryPid(); } -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() const { - Q_D(SingleApplication); - return d->primaryPid(); + Q_D( const SingleApplication ); + return d->primaryUser(); } -bool SingleApplication::sendMessage( QByteArray message, int timeout ) +/** + * Returns the username the current instance is running as. + * @return Returns the username the current instance is running as. + */ +QString SingleApplication::currentUser() const { - Q_D(SingleApplication); - - // Nobody to connect to - if( isPrimary() ) return false; - - // Make sure the socket is connected - d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ); - - d->socket->write( message ); - bool dataWritten = d->socket->waitForBytesWritten( timeout ); - d->socket->flush(); - return dataWritten; + return SingleApplicationPrivate::getUsername(); +} + +/** + * Sends message to the Primary Instance. + * @param message The message to send. + * @param timeout the maximum timeout in milliseconds for blocking functions. + * @return true if the message was sent successfuly, false otherwise. + */ +bool SingleApplication::sendMessage( const QByteArray &message, 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; + + d->socket->write( message ); + bool dataWritten = d->socket->waitForBytesWritten( timeout ); + d->socket->flush(); + return dataWritten; +} + +/** + * 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 ); +} + +QStringList SingleApplication::userData() const +{ + Q_D( const SingleApplication ); + return d->appData(); } diff --git a/SingleApplication/singleapplication.h b/SingleApplication/singleapplication.h index 5a9ab8318..91cabf93e 100644 --- a/SingleApplication/singleapplication.h +++ b/SingleApplication/singleapplication.h @@ -25,7 +25,6 @@ #include #include -#include #ifndef QAPPLICATION_CLASS #define QAPPLICATION_CLASS QCoreApplication @@ -36,99 +35,118 @@ class SingleApplicationPrivate; /** - @brief The SingleApplication class handles multiple instances of the same - Application - @see QCoreApplication -*/ + * @brief The SingleApplication class handles multiple instances of the same + * Application + * @see QCoreApplication + */ class SingleApplication : public QAPPLICATION_CLASS { - Q_OBJECT - - typedef QAPPLICATION_CLASS app_t; - - public: - /** - @brief Mode of operation of SingleApplication. - Whether the block should be user-wide or system-wide and whether the - primary instance should be notified when a secondary instance had been - started. - @note Operating system can restrict the shared memory blocks to the same - user, in which case the User/System modes will have no effect and the - block will be user wide. - @enum - */ - enum Mode { - User = 1 << 0, - System = 1 << 1, - SecondaryNotification = 1 << 2, - ExcludeAppVersion = 1 << 3, - ExcludeAppPath = 1 << 4 - }; - Q_DECLARE_FLAGS(Options, Mode) - - /** - @brief Intitializes a SingleApplication instance with argc command line - arguments in argv - @arg {int &} argc - Number of arguments in argv - @arg {const char *[]} argv - Supplied command line arguments - @arg {bool} allowSecondary - Whether to start the instance as secondary - if there is already a primary instance. - @arg {Mode} mode - Whether for the SingleApplication block to be applied - User wide or System wide. - @arg {int} timeout - Timeout to wait in milliseconds. - @note argc and argv may be changed as Qt removes arguments that it - recognizes - @note Mode::SecondaryNotification only works if set on both the primary - instance and the secondary instance. - @note The timeout is just a hint for the maximum time of blocking - operations. It does not guarantee that the SingleApplication - 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 ); - ~SingleApplication(); - - /** - @brief Returns if the instance is the primary instance - @returns {bool} - */ - bool isPrimary(); - - /** - @brief Returns if the instance is a secondary instance - @returns {bool} - */ - bool isSecondary(); - - /** - @brief Returns a unique identifier for the current instance - @returns {qint32} - */ - quint32 instanceId(); - - /** - @brief Returns the process ID (PID) of the primary instance - @returns {qint64} - */ - qint64 primaryPid(); - - /** - @brief Sends a message to the primary instance. Returns true on success. - @param {int} timeout - Timeout for connecting - @returns {bool} - @note sendMessage() will return false if invoked from the primary - instance. - */ - bool sendMessage( QByteArray message, int timeout = 100 ); - - Q_SIGNALS: - void instanceStarted(); - void receivedMessage( quint32 instanceId, QByteArray message ); - - private: - SingleApplicationPrivate *d_ptr; - Q_DECLARE_PRIVATE(SingleApplication) + Q_OBJECT + + using app_t = QAPPLICATION_CLASS; + +public: + /** + * @brief Mode of operation of SingleApplication. + * Whether the block should be user-wide or system-wide and whether the + * primary instance should be notified when a secondary instance had been + * started. + * @note Operating system can restrict the shared memory blocks to the same + * user, in which case the User/System modes will have no effect and the + * block will be user wide. + * @enum + */ + enum Mode { + User = 1 << 0, + System = 1 << 1, + SecondaryNotification = 1 << 2, + ExcludeAppVersion = 1 << 3, + ExcludeAppPath = 1 << 4 + }; + Q_DECLARE_FLAGS(Options, Mode) + + /** + * @brief Intitializes a SingleApplication instance with argc command line + * arguments in argv + * @arg {int &} argc - Number of arguments in argv + * @arg {const char *[]} argv - Supplied command line arguments + * @arg {bool} allowSecondary - Whether to start the instance as secondary + * if there is already a primary instance. + * @arg {Mode} mode - Whether for the SingleApplication block to be applied + * User wide or System wide. + * @arg {int} timeout - Timeout to wait in milliseconds. + * @note argc and argv may be changed as Qt removes arguments that it + * recognizes + * @note Mode::SecondaryNotification only works if set on both the primary + * instance and the secondary instance. + * @note The timeout is just a hint for the maximum time of blocking + * operations. It does not guarantee that the SingleApplication + * 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, const QString &userData = {} ); + ~SingleApplication() override; + + /** + * @brief Returns if the instance is the primary instance + * @returns {bool} + */ + bool isPrimary() const; + + /** + * @brief Returns if the instance is a secondary instance + * @returns {bool} + */ + bool isSecondary() const; + + /** + * @brief Returns a unique identifier for the current instance + * @returns {qint32} + */ + quint32 instanceId() const; + + /** + * @brief Returns the process ID (PID) of the primary instance + * @returns {qint64} + */ + qint64 primaryPid() const; + + /** + * @brief Returns the username of the user running the primary instance + * @returns {QString} + */ + QString primaryUser() const; + + /** + * @brief Returns the username of the current user + * @returns {QString} + */ + QString currentUser() const; + + /** + * @brief Sends a message to the primary instance. Returns true on success. + * @param {int} timeout - Timeout for connecting + * @returns {bool} + * @note sendMessage() will return false if invoked from the primary + * instance. + */ + bool sendMessage( const QByteArray &message, int timeout = 100 ); + + /** + * @brief Get the set user data. + * @returns {QStringList} + */ + QStringList userData() const; + +Q_SIGNALS: + void instanceStarted(); + void receivedMessage( quint32 instanceId, QByteArray message ); + +private: + SingleApplicationPrivate *d_ptr; + Q_DECLARE_PRIVATE(SingleApplication) + void abortSafely(); }; Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options) diff --git a/SingleApplication/singleapplication.pri b/SingleApplication/singleapplication.pri index c37471304..ae81f5997 100644 --- a/SingleApplication/singleapplication.pri +++ b/SingleApplication/singleapplication.pri @@ -1,7 +1,8 @@ QT += core network -CONFIG += c++17 +CONFIG += c++11 -HEADERS += $$PWD/singleapplication.h \ +HEADERS += $$PWD/SingleApplication \ + $$PWD/singleapplication.h \ $$PWD/singleapplication_p.h SOURCES += $$PWD/singleapplication.cpp \ $$PWD/singleapplication_p.cpp diff --git a/SingleApplication/singleapplication_p.cpp b/SingleApplication/singleapplication_p.cpp index b3553b0ee..e65bd9554 100644 --- a/SingleApplication/singleapplication_p.cpp +++ b/SingleApplication/singleapplication_p.cpp @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) Itay Grudev 2015 - 2018 +// 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 @@ -33,412 +33,454 @@ #include #include +#include #include #include +#include #include #include #include +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) +#include +#else +#include +#endif + #include "singleapplication.h" #include "singleapplication_p.h" #ifdef Q_OS_UNIX - #include - #include - #include + #include + #include + #include #endif #ifdef Q_OS_WIN -#include -#include + #ifndef NOMINMAX + #define NOMINMAX 1 + #endif + #include + #include #endif SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr ) - : q_ptr( q_ptr ) + : q_ptr( q_ptr ) { - server = nullptr; - socket = nullptr; - memory = nullptr; - instanceNumber = -1; + server = nullptr; + socket = nullptr; + memory = nullptr; + instanceNumber = 0; } SingleApplicationPrivate::~SingleApplicationPrivate() { - if( socket != nullptr ) { - socket->close(); - delete socket; - } + if( socket != nullptr ){ + socket->close(); + delete socket; + } - memory->lock(); - InstancesInfo* inst = static_cast(memory->data()); - if( server != nullptr ) { - server->close(); - delete server; - inst->primary = false; - inst->primaryPid = -1; - inst->checksum = blockChecksum(); - } - memory->unlock(); + 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; + delete memory; + } +} + +QString SingleApplicationPrivate::getUsername() +{ +#ifdef Q_OS_WIN + wchar_t username[UNLEN + 1]; + // Specifies size of the buffer on input + DWORD usernameLength = UNLEN + 1; + if( GetUserNameW( username, &usernameLength ) ) + return QString::fromWCharArray( username ); +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + return QString::fromLocal8Bit( qgetenv( "USERNAME" ) ); +#else + return qEnvironmentVariable( "USERNAME" ); +#endif +#endif +#ifdef Q_OS_UNIX + QString username; + uid_t uid = geteuid(); + struct passwd *pw = getpwuid( uid ); + if( pw ) + username = QString::fromLocal8Bit( pw->pw_name ); + if ( username.isEmpty() ){ +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + username = QString::fromLocal8Bit( qgetenv( "USER" ) ); +#else + username = qEnvironmentVariable( "USER" ); +#endif + } + return username; +#endif } void SingleApplicationPrivate::genBlockServerName() { - QCryptographicHash appData( QCryptographicHash::Sha256 ); - appData.addData( "SingleApplication", 17 ); - appData.addData( SingleApplication::app_t::applicationName().toUtf8() ); - appData.addData( SingleApplication::app_t::organizationName().toUtf8() ); - appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() ); + QCryptographicHash appData( QCryptographicHash::Sha256 ); + appData.addData( "SingleApplication", 17 ); + appData.addData( SingleApplication::app_t::applicationName().toUtf8() ); + appData.addData( SingleApplication::app_t::organizationName().toUtf8() ); + appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() ); - if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ) { - appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() ); - } + if ( ! appDataList.isEmpty() ) + appData.addData( appDataList.join( "" ).toUtf8() ); - if( ! (options & SingleApplication::Mode::ExcludeAppPath) ) { + if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ){ + appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() ); + } + + if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){ #ifdef Q_OS_WIN - appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); + appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); #else - appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); + appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); #endif - } + } - // User level block requires a user specific data in the hash - if( options & SingleApplication::Mode::User ) { -#ifdef Q_OS_WIN - wchar_t username [ UNLEN + 1 ]; - // Specifies size of the buffer on input - DWORD usernameLength = UNLEN + 1; - if( GetUserNameW( username, &usernameLength ) ) { - appData.addData( QString::fromWCharArray(username).toUtf8() ); - } else { - appData.addData( qgetenv("USERNAME") ); - } -#endif -#ifdef Q_OS_UNIX - QByteArray username; - uid_t uid = geteuid(); - struct passwd *pw = getpwuid(uid); - if( pw ) { - username = pw->pw_name; - } - if( username.isEmpty() ) { - username = qgetenv("USER"); - } - appData.addData(username); -#endif - } + // User level block requires a user specific data in the hash + if( options & SingleApplication::Mode::User ){ + appData.addData( getUsername().toUtf8() ); + } - // Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with - // server naming requirements. - blockServerName = appData.result().toBase64().replace("/", "_"); + // Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with + // server naming requirements. + blockServerName = appData.result().toBase64().replace("/", "_"); } -void SingleApplicationPrivate::initializeMemoryBlock() +void SingleApplicationPrivate::initializeMemoryBlock() const { - InstancesInfo* inst = static_cast( memory->data() ); - inst->primary = false; - inst->secondary = 0; - inst->primaryPid = -1; - inst->checksum = blockChecksum(); + 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() { - Q_Q(SingleApplication); + // Reset the number of connections + auto *inst = static_cast ( memory->data() ); - // Successful creation means that no main process exists - // So we start a QLocalServer to listen for connections - QLocalServer::removeServer( blockServerName ); - server = new QLocalServer(); + 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 ); + server = new QLocalServer(); - // Restrict access to the socket according to the - // SingleApplication::Mode::User flag on User level or no restrictions - if( options & SingleApplication::Mode::User ) { - server->setSocketOptions( QLocalServer::UserAccessOption ); - } else { - server->setSocketOptions( QLocalServer::WorldAccessOption ); - } + // Restrict access to the socket according to the + // SingleApplication::Mode::User flag on User level or no restrictions + if( options & SingleApplication::Mode::User ){ + server->setSocketOptions( QLocalServer::UserAccessOption ); + } else { + server->setSocketOptions( QLocalServer::WorldAccessOption ); + } - server->listen( blockServerName ); - QObject::connect( - server, - &QLocalServer::newConnection, - this, - &SingleApplicationPrivate::slotConnectionEstablished - ); - - // Reset the number of connections - InstancesInfo* inst = static_cast ( memory->data() ); - - inst->primary = true; - inst->primaryPid = q->applicationPid(); - inst->checksum = blockChecksum(); - - instanceNumber = 0; + server->listen( blockServerName ); + QObject::connect( + server, + &QLocalServer::newConnection, + this, + &SingleApplicationPrivate::slotConnectionEstablished + ); } void SingleApplicationPrivate::startSecondary() { + auto *inst = static_cast ( memory->data() ); + + inst->secondary += 1; + inst->checksum = blockChecksum(); + instanceNumber = inst->secondary; } -void SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType ) +bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType ) { - // Connect to the Local Server of the Primary Instance if not already - // connected. - if( socket == nullptr ) { - socket = new QLocalSocket(); - } + QElapsedTimer time; + time.start(); - // If already connected - we are done; - if( socket->state() == QLocalSocket::ConnectedState ) - return; + // Connect to the Local Server of the Primary Instance if not already + // connected. + if( socket == nullptr ){ + socket = new QLocalSocket(); + } - // If not connect - if( socket->state() == QLocalSocket::UnconnectedState || - socket->state() == QLocalSocket::ClosingState ) { - socket->connectToServer( blockServerName ); - } + if( socket->state() == QLocalSocket::ConnectedState ) return true; - // Wait for being connected - if( socket->state() == QLocalSocket::ConnectingState ) { - socket->waitForConnected( msecs ); - } + if( socket->state() != QLocalSocket::ConnectedState ){ - // Initialisation message according to the SingleApplication protocol - if( socket->state() == QLocalSocket::ConnectedState ) { - // Notify the parent that a new instance had been started; - QByteArray initMsg; - QDataStream writeStream(&initMsg, QIODevice::WriteOnly); + 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; + + // 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); + 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) // ### Qt 6: remove - quint16 checksum = - qChecksum( - initMsg.constData(), - static_cast(initMsg.length())); + writeStream << blockServerName.toLatin1(); + writeStream << static_cast(connectionType); + writeStream << instanceNumber; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + quint16 checksum = qChecksum(QByteArray(initMsg, static_cast(initMsg.length()))); #else -#if TODO_LIST -#pragma message("@TODO remove code for QT 6 or later") + quint16 checksum = qChecksum(initMsg.constData(), static_cast(initMsg.length())); #endif - quint16 checksum = - qChecksum( - QByteArrayView( - initMsg.constData(), - static_cast(initMsg.length()))); -#endif - writeStream << checksum; + writeStream << checksum; - // The header indicates the message length that follows - QByteArray header; - QDataStream headerStream(&header, QIODevice::WriteOnly); + // The header indicates the message length that follows + QByteArray header; + QDataStream headerStream(&header, QIODevice::WriteOnly); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion(QDataStream::Qt_5_6); + headerStream.setVersion(QDataStream::Qt_5_6); #endif - headerStream << static_cast ( initMsg.length() ); + headerStream << static_cast ( initMsg.length() ); - socket->write( header ); - socket->write( initMsg ); - socket->flush(); - socket->waitForBytesWritten( msecs ); - } + socket->write( header ); + socket->write( initMsg ); + bool result = socket->waitForBytesWritten( static_cast(msecs - time.elapsed()) ); + socket->flush(); + return result; } -quint16 SingleApplicationPrivate::blockChecksum() +quint16 SingleApplicationPrivate::blockChecksum() const { -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove - return qChecksum( - static_cast ( memory->data() ), - offsetof( InstancesInfo, checksum ) - ); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + quint16 checksum = qChecksum(QByteArray(static_cast(memory->constData()), offsetof(InstancesInfo, checksum))); #else -#if TODO_LIST -#pragma message("@TODO remove code for QT 6 or later") -#endif - return qChecksum( - QByteArrayView( - static_cast ( memory->data() ), - offsetof( InstancesInfo, checksum ))); - + quint16 checksum = qChecksum(static_cast(memory->constData()), offsetof(InstancesInfo, checksum)); #endif + return checksum; } -qint64 SingleApplicationPrivate::primaryPid() +qint64 SingleApplicationPrivate::primaryPid() const { - qint64 pid; + qint64 pid; - memory->lock(); - InstancesInfo* inst = static_cast( memory->data() ); - pid = inst->primaryPid; - memory->unlock(); + memory->lock(); + auto *inst = static_cast( memory->data() ); + pid = inst->primaryPid; + memory->unlock(); - return pid; + return pid; +} + +QString SingleApplicationPrivate::primaryUser() const +{ + QByteArray username; + + memory->lock(); + auto *inst = static_cast( memory->data() ); + username = inst->primaryUser; + memory->unlock(); + + return QString::fromUtf8( username ); } /** - @brief Executed when a connection has been made to the LocalServer -*/ + * @brief Executed when a connection has been made to the LocalServer + */ void SingleApplicationPrivate::slotConnectionEstablished() { - QLocalSocket *nextConnSocket = server->nextPendingConnection(); - connectionMap.insert(nextConnSocket, ConnectionInfo()); + QLocalSocket *nextConnSocket = server->nextPendingConnection(); + connectionMap.insert(nextConnSocket, ConnectionInfo()); - QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, - [nextConnSocket, this]() { - auto &info = connectionMap[nextConnSocket]; - Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); - } - ); + QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, + [nextConnSocket, this](){ + auto &info = connectionMap[nextConnSocket]; + Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); + } + ); - QObject::connect(nextConnSocket, &QLocalSocket::disconnected, - [nextConnSocket, this](){ - connectionMap.remove(nextConnSocket); - nextConnSocket->deleteLater(); - } - ); + QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater); - QObject::connect(nextConnSocket, &QLocalSocket::readyRead, - [nextConnSocket, this]() { - auto &info = connectionMap[nextConnSocket]; - switch(info.stage) { - case StageHeader: - readInitMessageHeader(nextConnSocket); - break; - case StageBody: - readInitMessageBody(nextConnSocket); - break; - case StageConnected: - Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId ); - break; - default: - break; - }; - } - ); + QObject::connect(nextConnSocket, &QLocalSocket::destroyed, + [nextConnSocket, this](){ + connectionMap.remove(nextConnSocket); + } + ); + + QObject::connect(nextConnSocket, &QLocalSocket::readyRead, + [nextConnSocket, this](){ + auto &info = connectionMap[nextConnSocket]; + switch(info.stage){ + case StageHeader: + readInitMessageHeader(nextConnSocket); + break; + case StageBody: + readInitMessageBody(nextConnSocket); + break; + case StageConnected: + Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId ); + break; + default: + break; + }; + } + ); } void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock ) { - if (!connectionMap.contains( sock )) { - return; - } + if (!connectionMap.contains( sock )){ + return; + } - if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ) { - return; - } + if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){ + return; + } - QDataStream headerStream( sock ); + QDataStream headerStream( sock ); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion( QDataStream::Qt_5_6 ); + 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 = StageBody; - info.msgLen = msgLen; + // Read the header to know the message length + quint64 msgLen = 0; + headerStream >> msgLen; + ConnectionInfo &info = connectionMap[sock]; + info.stage = StageBody; + info.msgLen = msgLen; - if ( sock->bytesAvailable() >= (qint64) msgLen ) { - readInitMessageBody( sock ); - } + if ( sock->bytesAvailable() >= (qint64) msgLen ){ + readInitMessageBody( sock ); + } } void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) { - Q_Q(SingleApplication); + Q_Q(SingleApplication); - if (!connectionMap.contains( sock )) { - return; - } + if (!connectionMap.contains( sock )){ + return; + } - ConnectionInfo &info = connectionMap[sock]; - if( sock->bytesAvailable() < ( qint64 )info.msgLen ) { - return; - } + ConnectionInfo &info = connectionMap[sock]; + if( sock->bytesAvailable() < ( qint64 )info.msgLen ){ + return; + } - // Read the message body - QByteArray msgBytes = sock->read(info.msgLen); - QDataStream readStream(msgBytes); + // Read the message body + QByteArray msgBytes = sock->read(info.msgLen); + QDataStream readStream(msgBytes); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - readStream.setVersion( QDataStream::Qt_5_6 ); + readStream.setVersion( QDataStream::Qt_5_6 ); #endif - // server name - QByteArray latin1Name; - readStream >> latin1Name; + // server name + QByteArray latin1Name; + readStream >> latin1Name; - // connection type - ConnectionType connectionType = InvalidConnection; - quint8 connTypeVal = InvalidConnection; - readStream >> connTypeVal; - connectionType = static_cast ( connTypeVal ); + // connection type + ConnectionType connectionType = InvalidConnection; + quint8 connTypeVal = InvalidConnection; + readStream >> connTypeVal; + connectionType = static_cast ( connTypeVal ); - // instance id - quint32 instanceId = 0; - readStream >> instanceId; + // instance id + quint32 instanceId = 0; + readStream >> instanceId; - // checksum - quint16 msgChecksum = 0; - readStream >> msgChecksum; + // checksum + quint16 msgChecksum = 0; + readStream >> msgChecksum; -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove - const quint16 actualChecksum = - qChecksum( - msgBytes.constData(), - static_cast( msgBytes.length() - sizeof( quint16 ) ) ); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast(msgBytes.length() - sizeof(quint16)))); #else -#if TODO_LIST -#pragma message("@TODO remove code for QT 6 or later") + const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast(msgBytes.length() - sizeof(quint16))); #endif - const quint16 actualChecksum = - qChecksum( - QByteArrayView( - 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; - } + bool isValid = readStream.status() == QDataStream::Ok && + QLatin1String(latin1Name) == blockServerName && + msgChecksum == actualChecksum; - info.instanceId = instanceId; - info.stage = StageConnected; + if( !isValid ){ + sock->close(); + return; + } - if( connectionType == NewInstance || - ( connectionType == SecondaryInstance && - options & SingleApplication::Mode::SecondaryNotification ) ) - { - Q_EMIT q->instanceStarted(); - } + info.instanceId = instanceId; + info.stage = StageConnected; - if (sock->bytesAvailable() > 0) { - Q_EMIT this->slotDataAvailable( sock, instanceId ); - } + if( connectionType == NewInstance || + ( connectionType == SecondaryInstance && + options & SingleApplication::Mode::SecondaryNotification ) ) + { + Q_EMIT q->instanceStarted(); + } + + if (sock->bytesAvailable() > 0){ + Q_EMIT this->slotDataAvailable( sock, instanceId ); + } } void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId ) { - Q_Q(SingleApplication); - Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() ); + Q_Q(SingleApplication); + Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() ); } void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId ) { - if( closedSocket->bytesAvailable() > 0 ) - Q_EMIT slotDataAvailable( closedSocket, instanceId ); + if( closedSocket->bytesAvailable() > 0 ) + Q_EMIT slotDataAvailable( closedSocket, instanceId ); +} + +void SingleApplicationPrivate::randomSleep() +{ +#if QT_VERSION >= QT_VERSION_CHECK( 5, 10, 0 ) + QThread::msleep( QRandomGenerator::global()->bounded( 8u, 18u )); +#else + qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits::max() ); + QThread::msleep( 8 + static_cast ( static_cast ( qrand() ) / RAND_MAX * 10 )); +#endif +} + +void SingleApplicationPrivate::addAppData(const QString &data) +{ + appDataList.push_back(data); +} + +QStringList SingleApplicationPrivate::appData() const +{ + return appDataList; } diff --git a/SingleApplication/singleapplication_p.h b/SingleApplication/singleapplication_p.h index ccbd56620..c49a46ddc 100644 --- a/SingleApplication/singleapplication_p.h +++ b/SingleApplication/singleapplication_p.h @@ -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 @@ -38,62 +38,67 @@ #include "singleapplication.h" struct InstancesInfo { - bool primary; - quint32 secondary; - qint64 primaryPid; - quint16 checksum; + bool primary; + quint32 secondary; + qint64 primaryPid; + char primaryUser[128]; + quint16 checksum; // Must be the last field }; struct ConnectionInfo { - explicit ConnectionInfo() : - msgLen(0), instanceId(0), stage(0) {} - qint64 msgLen; - quint32 instanceId; - quint8 stage; + qint64 msgLen = 0; + quint32 instanceId = 0; + quint8 stage = 0; }; class SingleApplicationPrivate : public QObject { - Q_OBJECT - public: - enum ConnectionType : quint8 { - InvalidConnection = 0, - NewInstance = 1, - SecondaryInstance = 2, - Reconnect = 3 - }; - enum ConnectionStage : quint8 { - StageHeader = 0, - StageBody = 1, - StageConnected = 2, - }; - Q_DECLARE_PUBLIC(SingleApplication) - - SingleApplicationPrivate( SingleApplication *q_ptr ); - ~SingleApplicationPrivate(); - - void genBlockServerName(); - void initializeMemoryBlock(); - void startPrimary(); - void startSecondary(); - void connectToPrimary(int msecs, ConnectionType connectionType ); - quint16 blockChecksum(); - qint64 primaryPid(); - void readInitMessageHeader(QLocalSocket *socket); - void readInitMessageBody(QLocalSocket *socket); - - SingleApplication *q_ptr; - QSharedMemory *memory; - QLocalSocket *socket; - QLocalServer *server; - quint32 instanceNumber; - QString blockServerName; - SingleApplication::Options options; - QMap connectionMap; - - public Q_SLOTS: - void slotConnectionEstablished(); - void slotDataAvailable( QLocalSocket*, quint32 ); - void slotClientConnectionClosed( QLocalSocket*, quint32 ); +Q_OBJECT +public: + enum ConnectionType : quint8 { + InvalidConnection = 0, + NewInstance = 1, + SecondaryInstance = 2, + Reconnect = 3 + }; + enum ConnectionStage : quint8 { + StageHeader = 0, + StageBody = 1, + StageConnected = 2, + }; + Q_DECLARE_PUBLIC(SingleApplication) + + SingleApplicationPrivate( SingleApplication *q_ptr ); + ~SingleApplicationPrivate() override; + + static QString getUsername(); + void genBlockServerName(); + void initializeMemoryBlock() const; + void startPrimary(); + void startSecondary(); + bool connectToPrimary( int msecs, ConnectionType connectionType ); + quint16 blockChecksum() const; + qint64 primaryPid() const; + QString primaryUser() const; + void readInitMessageHeader(QLocalSocket *socket); + void readInitMessageBody(QLocalSocket *socket); + static void randomSleep(); + void addAppData(const QString &data); + QStringList appData() const; + + SingleApplication *q_ptr; + QSharedMemory *memory; + QLocalSocket *socket; + QLocalServer *server; + quint32 instanceNumber; + QString blockServerName; + SingleApplication::Options options; + QMap connectionMap; + QStringList appDataList; + +public Q_SLOTS: + void slotConnectionEstablished(); + void slotDataAvailable( QLocalSocket*, quint32 ); + void slotClientConnectionClosed( QLocalSocket*, quint32 ); }; #endif // SINGLEAPPLICATION_P_H