Enable app class independent usage (#203)
Some checks failed
CI: Build Test / Build (-Wall -Wextra -pedantic -Werror, -j2, -D QT_DEFAULT_MAJOR_VERSION=6, make, ubuntu-20.04, 6.2.4) (push) Failing after 9s
CI: Build Test / Build (-Wall -Wextra -pedantic -Werror, -j2, -D QT_DEFAULT_MAJOR_VERSION=6, make, ubuntu-20.04, 6.5.0) (push) Failing after 9s
CI: Build Test / Build (-Wall -Wextra -pedantic -Werror, -j2, make, ubuntu-20.04, 5.15.0) (push) Failing after 9s
Documentation / Doxygen (push) Failing after 3h14m57s
CI: Build Test / Build (-Wall -Wextra -pedantic -Werror, -j3, -D QT_DEFAULT_MAJOR_VERSION=6, make, macos-13, 6.2.4) (push) Has been cancelled
CI: Build Test / Build (-Wall -Wextra -pedantic -Werror, -j3, -D QT_DEFAULT_MAJOR_VERSION=6, make, macos-13, 6.5.0) (push) Has been cancelled
CI: Build Test / Build (-Wall -Wextra -pedantic -Werror, -j3, make, macos-13, 5.15.0) (push) Has been cancelled
CI: Build Test / Build (/W4 /WX /MP, -D QT_DEFAULT_MAJOR_VERSION=6, nmake, windows-latest, 6.2.4) (push) Has been cancelled
CI: Build Test / Build (/W4 /WX /MP, -D QT_DEFAULT_MAJOR_VERSION=6, nmake, windows-latest, 6.5.0) (push) Has been cancelled
CI: Build Test / Build (/W4 /WX /MP, nmake, windows-latest, 5.15.0) (push) Has been cancelled

This is a pure extension and fully backwards compatible.

* Adds support for running it as a pre-compiled library while still being able to
choose my `QXxxApplication` class at build time.
* Be able to decide at runtime whether to use the single
instance stuff (without starting a server and so on).
This commit is contained in:
Benjamin Buch 2025-01-24 15:05:53 +01:00 committed by GitHub
parent 565ebd1c3d
commit 0ba7b6ce42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 365 additions and 19 deletions

View File

@ -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/

View File

@ -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_

View File

@ -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 $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
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()

View File

@ -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 <QCoreApplication>
/**
* @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

View File

@ -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 <QApplication>
#include <SingleApplication.h>
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<SingleApplication>`
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:

View File

@ -0,0 +1,5 @@
include(CMakeFindDependencyMacro)
find_dependency(Qt@QT_DEFAULT_MAJOR_VERSION@ COMPONENTS Core Network REQUIRED)
include("${CMAKE_CURRENT_LIST_DIR}/SingleApplicationTargets.cmake")

View File

@ -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)

View File

@ -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 <singleapplication.h>
#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();
}

View File

@ -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 <QDebug>
#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;
}

View File

@ -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 <QObject>
class MessageReceiver : public QObject
{
Q_OBJECT
public:
explicit MessageReceiver(QObject *parent = 0);
public slots:
void receivedMessage( int instanceId, QByteArray message );
};
#endif // MESSAGERECEIVER_H

View File

@ -0,0 +1,9 @@
# Single Application implementation
include(../../singleapplication.pri)
DEFINES += QAPPLICATION_CLASS=FreeStandingSingleApplication
SOURCES += main.cpp \
messagereceiver.cpp
HEADERS += \
messagereceiver.h

View File

@ -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
}