diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f12d2e..5807380 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -80,6 +80,12 @@ jobs: cmake . ${{ matrix.additional_arguments }} cmake --build . + - name: Build separate_object example with CMake + working-directory: examples/separate_object/ + run: | + cmake . ${{ matrix.additional_arguments }} + cmake --build . + - name: Build windows_raise_widget example with CMake working-directory: examples/windows_raise_widget/ run: | @@ -107,6 +113,13 @@ jobs: qmake ${{ matrix.make }} + - name: Build separate_object example with QMake + if: ${{ !contains(matrix.platform, 'macos') }} + working-directory: examples/separate_object/ + run: | + qmake + ${{ matrix.make }} + - name: Build windows_raise_widget example with QMake if: ${{ !contains(matrix.platform, 'macos') }} working-directory: examples/windows_raise_widget/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1044c4c..88f50fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 3.6.0 + +* Freestanding mode where `SingleApplication` doesn't derive from `QCodeApplication` _Benjamin Buch_ +* CMake install with CMake config files for freestanding mode _Benjamin Buch_ + ## 3.5.1 * Bug Fix: Maximum QNativeIpcKey key size on macOS. - _Jonas Kvinge_ diff --git a/CMakeLists.txt b/CMakeLists.txt index 174d56c..092e5fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12.0) -project(SingleApplication LANGUAGES CXX DESCRIPTION "Replacement for QtSingleApplication") +project(SingleApplication VERSION 3.6.0 LANGUAGES CXX DESCRIPTION "Replacement for QtSingleApplication") set(CMAKE_AUTOMOC ON) @@ -8,12 +8,22 @@ add_library(${PROJECT_NAME} STATIC singleapplication.cpp singleapplication_p.cpp ) -add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) +# User configurable options 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() +if(NOT QAPPLICATION_CLASS) + set(QAPPLICATION_CLASS QCoreApplication CACHE STRING "Qt application base class or FreeStandingSingleApplication") +endif() + +option(SINGLEAPPLICATION_INSTALL OFF "Enable freestanding mode install including config files") + +if(SINGLEAPPLICATION_INSTALL AND NOT QAPPLICATION_CLASS STREQUAL "FreeStandingSingleApplication") + message(FATAL_ERROR "SINGLEAPPLICATION_INSTALL requires QAPPLICATION_CLASS == FreeStandingSingleApplication") +endif() + # Find dependencies set(QT_COMPONENTS Core Network) set(QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Core Qt${QT_DEFAULT_MAJOR_VERSION}::Network) @@ -24,8 +34,6 @@ if(QAPPLICATION_CLASS STREQUAL QApplication) elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication) list(APPEND QT_COMPONENTS Gui) list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Gui) -else() - set(QAPPLICATION_CLASS QCoreApplication) endif() find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED) @@ -41,8 +49,15 @@ 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}) +if(SINGLEAPPLICATION_INSTALL) + target_compile_definitions(${PROJECT_NAME} PRIVATE QAPPLICATION_CLASS=${QAPPLICATION_CLASS}) + target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_include_directories(${PROJECT_NAME} INTERFACE $) +else() + target_compile_definitions(${PROJECT_NAME} PUBLIC QAPPLICATION_CLASS=${QAPPLICATION_CLASS}) + target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +endif() + target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_CAST_TO_ASCII QT_NO_CAST_FROM_ASCII @@ -81,3 +96,52 @@ if(DOXYGEN_FOUND) README.md ) endif() + +if(SINGLEAPPLICATION_INSTALL) + # Create a header veriant where QAPPLICATION_CLASS is replaced with FreeStandingSingleApplication + file(READ "${CMAKE_CURRENT_SOURCE_DIR}/singleapplication.h" SINGLEAPPLICATION_H_CONTENT) + + string(REGEX REPLACE + "#ifndef QAPPLICATION_CLASS[^\n]*\n[ \t]*#define QAPPLICATION_CLASS QCoreApplication[^\n]*\n[ \t]*#endif[^\n]*\n" + "" + SINGLEAPPLICATION_H_CONTENT + "${SINGLEAPPLICATION_H_CONTENT}") + + string(REGEX REPLACE + "#include QT_STRINGIFY\\(QAPPLICATION_CLASS\\)" + "#include \"FreeStandingSingleApplication\"" + SINGLEAPPLICATION_H_CONTENT + "${SINGLEAPPLICATION_H_CONTENT}") + + string(REPLACE + "QAPPLICATION_CLASS" + "FreeStandingSingleApplication" + SINGLEAPPLICATION_H_CONTENT + "${SINGLEAPPLICATION_H_CONTENT}") + + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/singleapplication.h" "${SINGLEAPPLICATION_H_CONTENT}") + + # CMake install + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/singleapplication.h" "SingleApplication" "FreeStandingSingleApplication" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + + include(CMakePackageConfigHelpers) + write_basic_package_version_file( + "SingleApplicationConfigVersion.cmake" + VERSION "${PACKAGE_VERSION}" + COMPATIBILITY SameMajorVersion) + + configure_file("SingleApplicationConfig.cmake.in" "SingleApplicationConfig.cmake" @ONLY) + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/SingleApplicationConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/SingleApplicationConfigVersion.cmake" + DESTINATION "lib/cmake/SingleApplication") + + install(TARGETS SingleApplication EXPORT SingleApplicationTargets) + install(EXPORT SingleApplicationTargets + FILE "SingleApplicationTargets.cmake" + NAMESPACE "SingleApplication::" + DESTINATION "lib/cmake/SingleApplication") +else() + add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) +endif() diff --git a/FreeStandingSingleApplication b/FreeStandingSingleApplication new file mode 100644 index 0000000..ef56ec4 --- /dev/null +++ b/FreeStandingSingleApplication @@ -0,0 +1,41 @@ +// Copyright (c) Itay Grudev 2015 - 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 FREE_STANDING_SINGLE_APPLICATION_H +#define FREE_STANDING_SINGLE_APPLICATION_H + +#include + +/** + * @brief Fake Qt application base class + * Use this as base if you want to use SingleApplication as a free standing object that must be + * explicitly instanciated after your Qt application object. + * + * This enables you to use SingleApplication as a precompiled library and/or to decide at runtime + * if you want to use a SingleApplication instance or not. + */ +struct FreeStandingSingleApplication: QObject{ + FreeStandingSingleApplication( int&, char** ) {} +}; + +#endif diff --git a/README.md b/README.md index fac4cb8..ab67b20 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,67 @@ 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. +## Freestanding mode + +Traditionally, the functionality of this library is implemented as part of the Qt +application class. The base class is defined by the macro `QAPPLICATION_CLASS`. + +In freestanding mode, `SingleApplication` is not derived from a Qt application +class. Instead, an instance of a Qt application class is created as normal, +followed by a separate instance of the `SingleApplication` class. + +```cpp +#include +#include + +int main( int argc, char* argv[] ) +{ + // The normal application class with a type of your choice + QApplication app( argc, argv ); + + // Separate single application object (argc and argv are discarded) + SingleApplication single( argc, argv /*, options ...*/ ); + + // Do your stuff + + return app.exec(); +} +``` + +_Note:_ With the discarded arguments and the class name that sounds like a Qt +application class without being one, this looks like a workaround – it is a +workaround. For 4.x, the single instance functionality could be moved to +something like a `SingleManager` class, which would then be used to implement +`SingleApplication`. This can't be done in 3.x, because moving +`SingleApplication::Mode` to `SingleManager::Mode` would be a breaking change. + +To enable the freestanding mode set `QAPPLICATION_CLASS` to +`FreeStandingSingleApplication`. This is a fake base class with no additional +functionality. + +The standalone mode allows us to use a precompiled version of this library, +because we don't need the `QAPPLICATION_CLASS` macro to define our Qt application +class at build time. Furthermore, we can use `std::optional` +to decide at runtime whether we want single application functionality or not. + +Use the standard CMake workflow to create a precompiled static library version, +including CMake config files. + +```bash +cmake -DQAPPLICATION_CLASS=FreeStandingSingleApplication -DSINGLEAPPLICATION_INSTALL=ON SingleApplicationDir +cmake --build . +cmake --install +``` + +This can be used via: + +```cmake +find_package(SingleApplication REQUIRED) +target_link_libraries(YourTarget SingleApplication::SingleApplication) +``` + +_Note:_ The `QAPPLICATION_CLASS` macro is eliminated during CMake install. + ## Instance started signal The `SingleApplication` class implements a `instanceStarted()` signal. You can @@ -185,11 +246,13 @@ will replace the Primary one even if the Secondary flag has been set.* ## Examples -There are three examples provided in this repository: +There are five 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) +* Basic example that prevents a secondary instance from starting [`examples/basic`](examples/basic) +* An example of a graphical application raising it's parent window [`examples/calculator`](examples/calculator) +* A console application sending the primary instance it's command line parameters [`examples/sending_arguments`](examples/sending_arguments) +* A variant of `sending_arguments` where `SingleApplication`is used in freestanding mode [`examples/separate_object`](examples/separate_object) +* A graphical application with Windows specific additions raising it's parent window [`examples/windows_raise_widget`](examples/windows_raise_widget) ## Versioning @@ -212,7 +275,7 @@ instances running. ## License -This library and it's supporting documentation, with the exception of the Qt +This library and it's supporting documentation, with the exception of the Qt calculator examples which is distributed under the BSD license, are released under the terms of `The MIT License (MIT)` with an extra condition, that: diff --git a/SingleApplicationConfig.cmake.in b/SingleApplicationConfig.cmake.in new file mode 100644 index 0000000..a903ac5 --- /dev/null +++ b/SingleApplicationConfig.cmake.in @@ -0,0 +1,5 @@ +include(CMakeFindDependencyMacro) + +find_dependency(Qt@QT_DEFAULT_MAJOR_VERSION@ COMPONENTS Core Network REQUIRED) + +include("${CMAKE_CURRENT_LIST_DIR}/SingleApplicationTargets.cmake") diff --git a/examples/separate_object/CMakeLists.txt b/examples/separate_object/CMakeLists.txt new file mode 100644 index 0000000..38f70a4 --- /dev/null +++ b/examples/separate_object/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.7.0) + +project(separate_object LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +# SingleApplication base class +set(QAPPLICATION_CLASS FreeStandingSingleApplication) +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/examples/separate_object/main.cpp b/examples/separate_object/main.cpp new file mode 100644 index 0000000..5385b48 --- /dev/null +++ b/examples/separate_object/main.cpp @@ -0,0 +1,53 @@ +// Copyright (c) Itay Grudev 2015 - 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 +#include "messagereceiver.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication app( argc, argv ); + + // Separate single instance object (that allows secondary instances) + SingleApplication single_instance_guard( argc, argv, true ); + + MessageReceiver msgReceiver; + + // If this is a secondary instance + if( single_instance_guard.isSecondary() ) { + single_instance_guard.sendMessage( app.arguments().join(' ').toUtf8() ); + qDebug() << "App already running."; + qDebug() << "Primary instance PID: " << single_instance_guard.primaryPid(); + qDebug() << "Primary instance user: " << single_instance_guard.primaryUser(); + return 0; + } else { + QObject::connect( + &single_instance_guard, + &SingleApplication::receivedMessage, + &msgReceiver, + &MessageReceiver::receivedMessage + ); + } + + return app.exec(); +} diff --git a/examples/separate_object/messagereceiver.cpp b/examples/separate_object/messagereceiver.cpp new file mode 100644 index 0000000..686b918 --- /dev/null +++ b/examples/separate_object/messagereceiver.cpp @@ -0,0 +1,35 @@ +// Copyright (c) Itay Grudev 2015 - 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 +#include "messagereceiver.h" + +MessageReceiver::MessageReceiver(QObject *parent) : QObject(parent) +{ +} + +void MessageReceiver::receivedMessage(int instanceId, QByteArray message) +{ + qDebug() << "Received message from instance: " << instanceId; + qDebug() << "Message Text: " << message; +} diff --git a/examples/separate_object/messagereceiver.h b/examples/separate_object/messagereceiver.h new file mode 100644 index 0000000..9f15db3 --- /dev/null +++ b/examples/separate_object/messagereceiver.h @@ -0,0 +1,38 @@ +// Copyright (c) Itay Grudev 2015 - 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 MESSAGERECEIVER_H +#define MESSAGERECEIVER_H + +#include + +class MessageReceiver : public QObject +{ + Q_OBJECT +public: + explicit MessageReceiver(QObject *parent = 0); +public slots: + void receivedMessage( int instanceId, QByteArray message ); +}; + +#endif // MESSAGERECEIVER_H diff --git a/examples/separate_object/separate_object.pro b/examples/separate_object/separate_object.pro new file mode 100644 index 0000000..f597348 --- /dev/null +++ b/examples/separate_object/separate_object.pro @@ -0,0 +1,9 @@ +# Single Application implementation +include(../../singleapplication.pri) +DEFINES += QAPPLICATION_CLASS=FreeStandingSingleApplication + +SOURCES += main.cpp \ + messagereceiver.cpp + +HEADERS += \ + messagereceiver.h diff --git a/singleapplication_p.cpp b/singleapplication_p.cpp index 3709022..44bda8d 100644 --- a/singleapplication_p.cpp +++ b/singleapplication_p.cpp @@ -140,33 +140,33 @@ 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() ); - appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() ); + appData.addData( QCoreApplication::applicationName().toUtf8() ); + appData.addData( QCoreApplication::organizationName().toUtf8() ); + appData.addData( QCoreApplication::organizationDomain().toUtf8() ); if ( ! appDataList.isEmpty() ) appData.addData( appDataList.join(QString()).toUtf8() ); if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ){ - appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() ); + appData.addData( QCoreApplication::applicationVersion().toUtf8() ); } if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){ #if defined(Q_OS_WIN) - appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); + appData.addData( QCoreApplication::applicationFilePath().toLower().toUtf8() ); #elif defined(Q_OS_LINUX) // If the application is running as an AppImage then the APPIMAGE env var should be used // instead of applicationPath() as each instance is launched with its own executable path const QByteArray appImagePath = qgetenv( "APPIMAGE" ); if( appImagePath.isEmpty() ){ // Not running as AppImage: use path to executable file - appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); + appData.addData( QCoreApplication::applicationFilePath().toUtf8() ); } else { // Running as AppImage: Use absolute path to AppImage file appData.addData( appImagePath ); }; #else - appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); + appData.addData( QCoreApplication::applicationFilePath().toUtf8() ); #endif }