From 60affd29598c7eecfd159affdc83c0117928e1e8 Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 2 Jul 2023 23:33:12 +0200 Subject: [PATCH] Use SingleApplication --- .gitmodules | 4 - 3rdparty/kdsingleapplication/CMakeLists.txt | 8 - .../kdsingleapplication/KDSingleApplication | 1 - 3rdparty/singleapplication/CMakeLists.txt | 12 + 3rdparty/singleapplication/LICENSE | 24 + 3rdparty/singleapplication/README.md | 305 +++++++++++ 3rdparty/singleapplication/config.h.in | 2 + .../singleapplication/CMakeLists.txt | 18 + .../singleapplication/singleapplication.h | 13 + .../singleapplication/singleapplication_p.cpp | 509 ++++++++++++++++++ .../singleapplication/singleapplication_p.h | 117 ++++ .../singleapplication/singleapplication_t.cpp | 330 ++++++++++++ .../singleapplication/singleapplication_t.h | 172 ++++++ .../singlecoreapplication/CMakeLists.txt | 17 + .../singlecoreapplication.h | 13 + CMakeLists.txt | 28 +- src/core/mainwindow.cpp | 7 + src/core/mainwindow.h | 1 + src/main.cpp | 19 +- 19 files changed, 1555 insertions(+), 45 deletions(-) delete mode 100644 3rdparty/kdsingleapplication/CMakeLists.txt delete mode 160000 3rdparty/kdsingleapplication/KDSingleApplication create mode 100644 3rdparty/singleapplication/CMakeLists.txt create mode 100644 3rdparty/singleapplication/LICENSE create mode 100644 3rdparty/singleapplication/README.md create mode 100644 3rdparty/singleapplication/config.h.in create mode 100644 3rdparty/singleapplication/singleapplication/CMakeLists.txt create mode 100644 3rdparty/singleapplication/singleapplication/singleapplication.h create mode 100644 3rdparty/singleapplication/singleapplication_p.cpp create mode 100644 3rdparty/singleapplication/singleapplication_p.h create mode 100644 3rdparty/singleapplication/singleapplication_t.cpp create mode 100644 3rdparty/singleapplication/singleapplication_t.h create mode 100644 3rdparty/singleapplication/singlecoreapplication/CMakeLists.txt create mode 100644 3rdparty/singleapplication/singlecoreapplication/singlecoreapplication.h diff --git a/.gitmodules b/.gitmodules index 27c3a4b3..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +0,0 @@ -[submodule "3rdparty/kdsingleapplication/KDSingleApplication"] - path = 3rdparty/kdsingleapplication/KDSingleApplication - url = https://github.com/KDAB/KDSingleApplication.git - branch = master diff --git a/3rdparty/kdsingleapplication/CMakeLists.txt b/3rdparty/kdsingleapplication/CMakeLists.txt deleted file mode 100644 index 547ba1a1..00000000 --- a/3rdparty/kdsingleapplication/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -cmake_minimum_required(VERSION 3.7) -set(SOURCES KDSingleApplication/src/kdsingleapplication.cpp KDSingleApplication/src/kdsingleapplication_localsocket.cpp) -set(HEADERS KDSingleApplication/src/kdsingleapplication.h KDSingleApplication/src/kdsingleapplication_localsocket_p.h) -qt_wrap_cpp(MOC ${HEADERS}) -add_library(kdsingleapplication STATIC ${SOURCES} ${MOC}) -target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD) -target_include_directories(kdsingleapplication PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(kdsingleapplication PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network) diff --git a/3rdparty/kdsingleapplication/KDSingleApplication b/3rdparty/kdsingleapplication/KDSingleApplication deleted file mode 160000 index cb0c664b..00000000 --- a/3rdparty/kdsingleapplication/KDSingleApplication +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cb0c664b40d3b31bad30aa3521eff603162ed0bd diff --git a/3rdparty/singleapplication/CMakeLists.txt b/3rdparty/singleapplication/CMakeLists.txt new file mode 100644 index 00000000..2febf5aa --- /dev/null +++ b/3rdparty/singleapplication/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.7) + +include(CheckIncludeFiles) +include(CheckFunctionExists) + +check_function_exists(geteuid HAVE_GETEUID) +check_function_exists(getpwuid HAVE_GETPWUID) + +configure_file(config.h.in "${CMAKE_CURRENT_BINARY_DIR}/config.h") + +add_subdirectory(singleapplication) +add_subdirectory(singlecoreapplication) diff --git a/3rdparty/singleapplication/LICENSE b/3rdparty/singleapplication/LICENSE new file mode 100644 index 00000000..a82e5a68 --- /dev/null +++ b/3rdparty/singleapplication/LICENSE @@ -0,0 +1,24 @@ +The MIT License (MIT) + +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 +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: + +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. + +Note: Some of the examples include code not distributed under the terms of the +MIT License. diff --git a/3rdparty/singleapplication/README.md b/3rdparty/singleapplication/README.md new file mode 100644 index 00000000..716f9a7e --- /dev/null +++ b/3rdparty/singleapplication/README.md @@ -0,0 +1,305 @@ +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` 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) +instances and can send data to the primary instance from secondary instances. + +Usage +----- + +The `SingleApplication` class inherits from whatever `Q[Core|Gui]Application` +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. + +You can use the library as if you use any other `QCoreApplication` derived +class: + +```cpp +#include +#include + +int main( int argc, char* argv[] ) +{ + SingleApplication app( argc, argv ); + + return app.exec(); +} +``` + +To include the library files I would recommend that you add it as a git +submodule to your project. Here is how: + +```bash +git submodule add https://github.com/itay-grudev/SingleApplication.git singleapplication +``` + +**Qmake:** + +Then include the `singleapplication.pri` file in your `.pro` project file. + +```qmake +include(singleapplication/singleapplication.pri) +DEFINES += QAPPLICATION_CLASS=QApplication +``` + +**CMake:** + +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 +been started, for example. + +```cpp +// window is a QWindow instance +QObject::connect( + &app, + &SingleApplication::instanceStarted, + &window, + &QWindow::raise +); +``` + +Using `SingleApplication::instance()` is a neat way to get the +`SingleApplication` instance for binding to it's signals anywhere in your +program. + +__Note:__ On Windows the ability to bring the application windows to the +foreground is restricted. See [Windows specific implementations](Windows.md) +for a workaround and an example implementation. + + +Secondary Instances +------------------- + +If you want to be able to launch additional Secondary Instances (not related to +your Primary Instance) you have to enable that with the third parameter of the +`SingleApplication` constructor. The default is `false` meaning no Secondary +Instances. Here is an example of how you would start a Secondary Instance send +a message with the command line arguments to the primary instance and then shut +down. + +```cpp +int main(int argc, char *argv[]) +{ + SingleApplication app( argc, argv, true ); + + if( app.isSecondary() ) { + app.sendMessage( app.arguments().join(' ')).toUtf8() ); + app.exit( 0 ); + } + + return app.exec(); +} +``` + +*__Note:__ A secondary instance won't cause the emission of the +`instanceStarted()` signal by default. See `SingleApplication::Mode` for more +details.* + +You can check whether your instance is a primary or secondary with the following +methods: + +```cpp +app.isPrimary(); +// or +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, QString userData = QString() ) +``` + +Depending on whether `allowSecondary` is set, this constructor may terminate +your app if there is already a primary instance running. Additional `Options` +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. 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.* + +*__Note:__ `Mode::SecondaryNotification` only works if set on both the primary +and the secondary instance.* + +*__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.* + +--- + +```cpp +bool SingleApplication::sendMessage( QByteArray message, int timeout = 100 ) +``` + +Sends `message` to the Primary Instance. Uses `timeout` as a the maximum timeout +in milliseconds for blocking functions. Returns `true` if the message has been sent +successfully. If the message can't be sent or the function timeouts - returns `false`. + +--- + +```cpp +bool SingleApplication::isPrimary() +``` + +Returns if the instance is the primary instance. + +--- + +```cpp +bool SingleApplication::isSecondary() +``` +Returns if the instance is a secondary instance. + +--- + +```cpp +quint32 SingleApplication::instanceId() +``` + +Returns a unique identifier for the current instance. + +--- + +```cpp +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 +void SingleApplication::instanceStarted() +``` + +Triggered whenever a new instance had been started, except for secondary +instances if the `Mode::SecondaryNotification` flag is not specified. + +--- + +```cpp +void SingleApplication::receivedMessage( quint32 instanceId, QByteArray message ) +``` + +Triggered whenever there is a message received from a secondary instance. + +--- + +### Flags + +```cpp +enum SingleApplication::Mode +``` + +* `Mode::User` - The SingleApplication block should apply user wide. This adds + user specific data to the key used for the shared memory and server name. + This is the default functionality. +* `Mode::System` – The SingleApplication block applies system-wide. +* `Mode::SecondaryNotification` – Whether to trigger `instanceStarted()` even + whenever secondary instances are started. +* `Mode::ExcludeAppPath` – Excludes the application path from the server name + (and memory block) hash. +* `Mode::ExcludeAppVersion` – Excludes the application version from the server + name (and memory block) hash. + +*__Note:__ `Mode::SecondaryNotification` only works if set on both the primary +and the secondary instance.* + +*__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.* + +--- + +Versioning +---------- + +Each major version introduces either very significant changes or is not +backwards compatible with the previous version. Minor versions only add +additional features, bug fixes or performance improvements and are backwards +compatible with the previous release. See [`CHANGELOG.md`](CHANGELOG.md) for +more details. + +Implementation +-------------- + +The library is implemented with a QSharedMemory block which is thread safe and +guarantees a race condition will not occur. It also uses a QLocalSocket to +notify the main process that a new instance had been spawned and thus invoke the +`instanceStarted()` signal and for messaging the primary instance. + +Additionally the library can recover from being forcefully killed on *nix +systems and will reset the memory block given that there are no other +instances running. + +License +------- +This library and it's supporting documentation are released under +`The MIT License (MIT)` with the exception of the Qt calculator examples which +is distributed under the BSD license. diff --git a/3rdparty/singleapplication/config.h.in b/3rdparty/singleapplication/config.h.in new file mode 100644 index 00000000..e7c1343f --- /dev/null +++ b/3rdparty/singleapplication/config.h.in @@ -0,0 +1,2 @@ +#cmakedefine HAVE_GETEUID +#cmakedefine HAVE_GETPWUID diff --git a/3rdparty/singleapplication/singleapplication/CMakeLists.txt b/3rdparty/singleapplication/singleapplication/CMakeLists.txt new file mode 100644 index 00000000..d9d8fd0f --- /dev/null +++ b/3rdparty/singleapplication/singleapplication/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.7) + +add_definitions(-DSINGLEAPPLICATION) + +set(SOURCES ../singleapplication_t.cpp ../singleapplication_p.cpp) +set(HEADERS ../singleapplication_t.h ../singleapplication_p.h) +qt_wrap_cpp(MOC ${HEADERS}) +add_library(singleapplication STATIC ${SOURCES} ${MOC}) +target_include_directories(singleapplication PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_BINARY_DIR}/.. + ${Boost_INCLUDE_DIRS} +) +target_link_libraries(singleapplication PUBLIC + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Widgets +) diff --git a/3rdparty/singleapplication/singleapplication/singleapplication.h b/3rdparty/singleapplication/singleapplication/singleapplication.h new file mode 100644 index 00000000..34beb3ce --- /dev/null +++ b/3rdparty/singleapplication/singleapplication/singleapplication.h @@ -0,0 +1,13 @@ +#ifndef SINGLEAPPLICATION_H +#define SINGLEAPPLICATION_H + +#ifdef SINGLEAPPLICATION +# error "SINGLEAPPLICATION already defined." +#endif + +#define SINGLEAPPLICATION +#include "../singleapplication_t.h" +#undef SINGLEAPPLICATION_T_H +#undef SINGLEAPPLICATION + +#endif // SINGLEAPPLICATION_H diff --git a/3rdparty/singleapplication/singleapplication_p.cpp b/3rdparty/singleapplication/singleapplication_p.cpp new file mode 100644 index 00000000..c7c49d29 --- /dev/null +++ b/3rdparty/singleapplication/singleapplication_p.cpp @@ -0,0 +1,509 @@ +// The MIT License (MIT) +// +// 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 +// 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: +// +// 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. + +// +// W A R N I N G !!! +// ----------------- +// +// This is a modified version of SingleApplication, +// The original version is at: +// +// https://github.com/itay-grudev/SingleApplication +// +// + +#include "config.h" + +#include + +#include +#include + +#ifdef Q_OS_UNIX +# include +# include +# include +#endif + +#ifdef Q_OS_WIN +# ifndef NOMINMAX +# define NOMINMAX 1 +# endif +# include +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "singleapplication_t.h" +#include "singleapplication_p.h" + +SingleApplicationPrivateClass::SingleApplicationPrivateClass(SingleApplicationClass *ptr) + : q_ptr(ptr), + memory_(nullptr), + socket_(nullptr), + server_(nullptr), + instanceNumber_(-1) {} + +SingleApplicationPrivateClass::~SingleApplicationPrivateClass() { + + if (socket_ != nullptr && socket_->isOpen()) { + socket_->close(); + } + + if (memory_ != nullptr) { + memory_->lock(); + if (server_ != nullptr) { + server_->close(); + InstancesInfo *instance = static_cast(memory_->data()); + instance->primary = false; + instance->primaryPid = -1; + instance->primaryUser[0] = '\0'; + instance->checksum = blockChecksum(); + } + memory_->unlock(); + if (memory_->isAttached()) { + memory_->detach(); + } + } + +} + +QString SingleApplicationPrivateClass::getUsername() { + +#ifdef Q_OS_UNIX + QString username; +#if defined(HAVE_GETEUID) && defined(HAVE_GETPWUID) + struct passwd *pw = getpwuid(geteuid()); + if (pw) { + username = QString::fromLocal8Bit(pw->pw_name); + } +#endif + if (username.isEmpty()) { + username = qEnvironmentVariable("USER"); + } + return username; +#endif + +#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); + } + return qEnvironmentVariable("USERNAME"); +#endif + +} + +void SingleApplicationPrivateClass::genBlockServerName() { + +#ifdef Q_OS_MACOS + QCryptographicHash appData(QCryptographicHash::Md5); +#else + QCryptographicHash appData(QCryptographicHash::Sha256); +#endif + appData.addData("SingleApplication"); + appData.addData(SingleApplicationClass::applicationName().toUtf8()); + appData.addData(SingleApplicationClass::organizationName().toUtf8()); + appData.addData(SingleApplicationClass::organizationDomain().toUtf8()); + + if (!(options_ & SingleApplicationClass::Mode::ExcludeAppVersion)) { + appData.addData(SingleApplicationClass::applicationVersion().toUtf8()); + } + + if (!(options_ & SingleApplicationClass::Mode::ExcludeAppPath)) { +#if defined(Q_OS_UNIX) + const QByteArray appImagePath = qgetenv("APPIMAGE"); + if (appImagePath.isEmpty()) { + appData.addData(SingleApplicationClass::applicationFilePath().toUtf8()); + } + else { + appData.addData(appImagePath); + } +#elif defined(Q_OS_WIN) + appData.addData(SingleApplicationClass::applicationFilePath().toLower().toUtf8()); +#else + appData.addData(SingleApplicationClass::applicationFilePath().toUtf8()); +#endif + } + + // User level block requires a user specific data in the hash + if (options_ & SingleApplicationClass::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("/", "_"); + +} + +void SingleApplicationPrivateClass::initializeMemoryBlock() const { + + InstancesInfo *instance = static_cast(memory_->data()); + instance->primary = false; + instance->secondary = 0; + instance->primaryPid = -1; + instance->primaryUser[0] = '\0'; + instance->checksum = blockChecksum(); + +} + +void SingleApplicationPrivateClass::startPrimary() { + + // Reset the number of connections + InstancesInfo *instance = static_cast(memory_->data()); + + instance->primary = true; + instance->primaryPid = QCoreApplication::applicationPid(); + qstrncpy(instance->primaryUser, getUsername().toUtf8().data(), sizeof(instance->primaryUser)); + instance->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(this); + + // Restrict access to the socket according to the SingleApplication::Mode::User flag on User level or no restrictions + if (options_ & SingleApplicationClass::Mode::User) { + server_->setSocketOptions(QLocalServer::UserAccessOption); + } + else { + server_->setSocketOptions(QLocalServer::WorldAccessOption); + } + + server_->listen(blockServerName_); + QObject::connect(server_, &QLocalServer::newConnection, this, &SingleApplicationPrivateClass::slotConnectionEstablished); + +} + +void SingleApplicationPrivateClass::startSecondary() { + + InstancesInfo *instance = static_cast(memory_->data()); + + instance->secondary += 1; + instance->checksum = blockChecksum(); + instanceNumber_ = instance->secondary; + +} + +bool SingleApplicationPrivateClass::connectToPrimary(const int timeout, const ConnectionType connectionType) { + + // Connect to the Local Server of the Primary Instance if not already connected. + if (socket_ == nullptr) { + socket_ = new QLocalSocket(this); + } + + if (socket_->state() == QLocalSocket::ConnectedState) return true; + + QElapsedTimer time; + time.start(); + + if (socket_->state() != QLocalSocket::ConnectedState) { + + forever { + randomSleep(); + + if (socket_->state() != QLocalSocket::ConnectingState) { + socket_->connectToServer(blockServerName_); + } + + if (socket_->state() == QLocalSocket::ConnectingState) { + socket_->waitForConnected(static_cast(timeout - 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() >= timeout) return false; + } + } + + // Initialization message according to the SingleApplication protocol + QByteArray initMsg; + QDataStream writeStream(&initMsg, QIODevice::WriteOnly); + writeStream.setVersion(QDataStream::Qt_5_8); + + 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 + quint16 checksum = qChecksum(initMsg.constData(), static_cast(initMsg.length())); +#endif + + writeStream << checksum; + + return writeConfirmedMessage(static_cast(timeout - time.elapsed()), initMsg); + +} + +void SingleApplicationPrivateClass::writeAck(QLocalSocket *sock) { + sock->putChar('\n'); +} + +bool SingleApplicationPrivateClass::writeConfirmedMessage(const int timeout, const QByteArray &msg) const { + + QElapsedTimer time; + time.start(); + + // Frame 1: The header indicates the message length that follows + QByteArray header; + QDataStream headerStream(&header, QIODevice::WriteOnly); + headerStream.setVersion(QDataStream::Qt_5_8); + headerStream << static_cast(msg.length()); + + if (!writeConfirmedFrame(static_cast(timeout - time.elapsed()), header)) { + return false; + } + + // Frame 2: The message + return writeConfirmedFrame(static_cast(timeout - time.elapsed()), msg); + +} + +bool SingleApplicationPrivateClass::writeConfirmedFrame(const int timeout, const QByteArray &msg) const { + + socket_->write(msg); + socket_->flush(); + + bool result = socket_->waitForReadyRead(timeout); + if (result) { + socket_->read(1); + return true; + } + + return false; + +} + +quint16 SingleApplicationPrivateClass::blockChecksum() const { + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + quint16 checksum = qChecksum(QByteArray(static_cast(memory_->constData()), offsetof(InstancesInfo, checksum))); +#else + quint16 checksum = qChecksum(static_cast(memory_->constData()), offsetof(InstancesInfo, checksum)); +#endif + + return checksum; + +} + +qint64 SingleApplicationPrivateClass::primaryPid() const { + + memory_->lock(); + InstancesInfo *instance = static_cast(memory_->data()); + qint64 pid = instance->primaryPid; + memory_->unlock(); + + return pid; + +} + +QString SingleApplicationPrivateClass::primaryUser() const { + + memory_->lock(); + InstancesInfo *instance = static_cast(memory_->data()); + QByteArray username = instance->primaryUser; + memory_->unlock(); + + return QString::fromUtf8(username); + +} + +/** + * @brief Executed when a connection has been made to the LocalServer + */ +void SingleApplicationPrivateClass::slotConnectionEstablished() { + + QLocalSocket *nextConnSocket = server_->nextPendingConnection(); + connectionMap_.insert(nextConnSocket, ConnectionInfo()); + + QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [this, nextConnSocket]() { + const ConnectionInfo &info = connectionMap_[nextConnSocket]; + slotClientConnectionClosed(nextConnSocket, info.instanceId); + }); + + QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater); + + QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, [this, nextConnSocket]() { + connectionMap_.remove(nextConnSocket); + }); + + QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [this, nextConnSocket]() { + const ConnectionInfo &info = connectionMap_[nextConnSocket]; + switch (info.stage) { + case StageInitHeader: + readMessageHeader(nextConnSocket, StageInitBody); + break; + case StageInitBody: + readInitMessageBody(nextConnSocket); + break; + case StageConnectedHeader: + readMessageHeader(nextConnSocket, StageConnectedBody); + break; + case StageConnectedBody: + slotDataAvailable(nextConnSocket, info.instanceId); + break; + default: + break; + } + }); + +} + +void SingleApplicationPrivateClass::readMessageHeader(QLocalSocket *sock, const SingleApplicationPrivateClass::ConnectionStage nextStage) { + + if (!connectionMap_.contains(sock)) { + return; + } + + if (sock->bytesAvailable() < static_cast(sizeof(quint64))) { + return; + } + + QDataStream headerStream(sock); + headerStream.setVersion(QDataStream::Qt_5_8); + + // Read the header to know the message length + quint64 msgLen = 0; + headerStream >> msgLen; + ConnectionInfo &info = connectionMap_[sock]; + info.stage = nextStage; + info.msgLen = msgLen; + + writeAck(sock); + +} + +bool SingleApplicationPrivateClass::isFrameComplete(QLocalSocket *sock) { + + if (!connectionMap_.contains(sock)) { + return false; + } + + const ConnectionInfo &info = connectionMap_[sock]; + return (sock->bytesAvailable() >= static_cast(info.msgLen)); + +} + +void SingleApplicationPrivateClass::readInitMessageBody(QLocalSocket *sock) { + + Q_Q(SingleApplicationClass); + + if (!isFrameComplete(sock)) { + return; + } + + // Read the message body + QByteArray msgBytes = sock->readAll(); + QDataStream readStream(msgBytes); + readStream.setVersion(QDataStream::Qt_5_8); + + // server name + QByteArray latin1Name; + readStream >> latin1Name; + + // connection type + quint8 connTypeVal = InvalidConnection; + readStream >> connTypeVal; + const ConnectionType connectionType = static_cast(connTypeVal); + + // instance id + quint32 instanceId = 0; + readStream >> instanceId; + + // checksum + quint16 msgChecksum = 0; + readStream >> msgChecksum; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast(msgBytes.length() - sizeof(quint16)))); +#else + const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast(msgBytes.length() - sizeof(quint16))); +#endif + + bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName_ && msgChecksum == actualChecksum; + + if (!isValid) { + sock->close(); + return; + } + + ConnectionInfo &info = connectionMap_[sock]; + info.instanceId = instanceId; + info.stage = StageConnectedHeader; + + if (connectionType == NewInstance || (connectionType == SecondaryInstance && options_ & SingleApplicationClass::Mode::SecondaryNotification)) { + emit q->instanceStarted(); + } + + writeAck(sock); + +} + +void SingleApplicationPrivateClass::slotDataAvailable(QLocalSocket *dataSocket, const quint32 instanceId) { + + Q_Q(SingleApplicationClass); + + if (!isFrameComplete(dataSocket)) { + return; + } + + const QByteArray message = dataSocket->readAll(); + + writeAck(dataSocket); + + ConnectionInfo &info = connectionMap_[dataSocket]; + info.stage = StageConnectedHeader; + + emit q->receivedMessage(instanceId, message); + +} + +void SingleApplicationPrivateClass::slotClientConnectionClosed(QLocalSocket *closedSocket, const quint32 instanceId) { + + if (closedSocket->bytesAvailable() > 0) { + slotDataAvailable(closedSocket, instanceId); + } + +} + +void SingleApplicationPrivateClass::randomSleep() { + + QThread::msleep(QRandomGenerator::global()->bounded(8U, 18U)); + +} diff --git a/3rdparty/singleapplication/singleapplication_p.h b/3rdparty/singleapplication/singleapplication_p.h new file mode 100644 index 00000000..1c14c3c7 --- /dev/null +++ b/3rdparty/singleapplication/singleapplication_p.h @@ -0,0 +1,117 @@ +// The MIT License (MIT) +// +// 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 +// 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: +// +// 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. + +// +// W A R N I N G !!! +// ----------------- +// +// This is a modified version of SingleApplication, +// The original version is at: +// +// https://github.com/itay-grudev/SingleApplication +// +// + +#ifndef SINGLEAPPLICATION_P_H +#define SINGLEAPPLICATION_P_H + +#include +#include +#include +#include + +#include "singleapplication_t.h" + +class QLocalServer; +class QLocalSocket; +class QSharedMemory; + +class SingleApplicationPrivateClass : public QObject { + Q_OBJECT + + public: + explicit SingleApplicationPrivateClass(SingleApplicationClass *ptr); + ~SingleApplicationPrivateClass() override; + + enum ConnectionType : quint8 { + InvalidConnection = 0, + NewInstance = 1, + SecondaryInstance = 2, + Reconnect = 3 + }; + enum ConnectionStage : quint8 { + StageInitHeader = 0, + StageInitBody = 1, + StageConnectedHeader = 2, + StageConnectedBody = 3 + }; + Q_DECLARE_PUBLIC(SingleApplicationClass) + + struct InstancesInfo { + explicit InstancesInfo() : primary(false), secondary(0), primaryPid(0), checksum(0) {} + bool primary; + quint32 secondary; + qint64 primaryPid; + char primaryUser[128]; + quint16 checksum; + }; + + struct ConnectionInfo { + explicit ConnectionInfo() : msgLen(0), instanceId(0), stage(0) {} + quint64 msgLen; + quint32 instanceId; + quint8 stage; + }; + + static QString getUsername(); + void genBlockServerName(); + void initializeMemoryBlock() const; + void startPrimary(); + void startSecondary(); + bool connectToPrimary(const int timeout, const ConnectionType connectionType); + quint16 blockChecksum() const; + qint64 primaryPid() const; + QString primaryUser() const; + bool isFrameComplete(QLocalSocket *sock); + void readMessageHeader(QLocalSocket *socket, const ConnectionStage nextStage); + void readInitMessageBody(QLocalSocket *socket); + void writeAck(QLocalSocket *sock); + bool writeConfirmedFrame(const int timeout, const QByteArray &msg) const; + bool writeConfirmedMessage(const int timeout, const QByteArray &msg) const; + static void randomSleep(); + + SingleApplicationClass *q_ptr; + QSharedMemory *memory_; + QLocalSocket *socket_; + QLocalServer *server_; + quint32 instanceNumber_; + QString blockServerName_; + SingleApplicationClass::Options options_; + QHash connectionMap_; + + public slots: + void slotConnectionEstablished(); + void slotDataAvailable(QLocalSocket*, const quint32); + void slotClientConnectionClosed(QLocalSocket*, const quint32); +}; + +#endif // SINGLEAPPLICATION_P_H diff --git a/3rdparty/singleapplication/singleapplication_t.cpp b/3rdparty/singleapplication/singleapplication_t.cpp new file mode 100644 index 00000000..2a75dcde --- /dev/null +++ b/3rdparty/singleapplication/singleapplication_t.cpp @@ -0,0 +1,330 @@ +// The MIT License (MIT) +// +// 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 +// 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: +// +// 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. + +// +// W A R N I N G !!! +// ----------------- +// +// This is a modified version of SingleApplication, +// The original version is at: +// +// https://github.com/itay-grudev/SingleApplication +// +// + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) +# include +#endif + +#include "singleapplication_t.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 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 + */ +SingleApplicationClass::SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary, const Options options, const int timeout) + : ApplicationClass(argc, argv), + d_ptr(new SingleApplicationPrivateClass(this)) { + +#if defined(SINGLEAPPLICATION) + Q_D(SingleApplication); +#elif defined(SINGLECOREAPPLICATION) + Q_D(SingleCoreApplication); +#endif + + // 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(); + + // To mitigate QSharedMemory issues with large amount of processes attempting to attach at the same time + SingleApplicationPrivateClass::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. + { +# if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + std::unique_ptr memory = std::make_unique(QNativeIpcKey(d->blockServerName_)); +# else + std::unique_ptr memory = std::make_unique(d->blockServerName_); +# endif + if (memory->attach()) { + memory->detach(); + } + } +#endif + + // Guarantee thread safe behaviour with a shared memory block. +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + QSharedMemory *memory = new QSharedMemory(QNativeIpcKey(d->blockServerName_), this); +#else + QSharedMemory *memory = new QSharedMemory(d->blockServerName_, this); +#endif + d->memory_ = memory; + + bool primary = false; + + // Create a shared memory block + if (d->memory_->create(sizeof(SingleApplicationPrivateClass::InstancesInfo))) { + primary = true; + } + else if (d->memory_->error() == QSharedMemory::AlreadyExists) { + if (!d->memory_->attach()) { + qCritical() << "SingleApplication: Unable to attach to shared memory block:" << d->memory_->error() << d->memory_->errorString(); + return; + } + } + else { + qCritical() << "SingleApplication: Unable to create shared memory block:" << d->memory_->error() << d->memory_->errorString(); + return; + } + + bool locked = false; + + BOOST_SCOPE_EXIT((memory)(&locked)) { + if (locked && !memory->unlock()) { + qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString(); + return; + } + }BOOST_SCOPE_EXIT_END + + if (!d->memory_->lock()) { + qCritical() << "SingleApplication: Unable to lock shared memory block:" << d->memory_->error() << d->memory_->errorString(); + return; + } + locked = true; + + if (primary) { + // Initialize the shared memory block + d->initializeMemoryBlock(); + } + + SingleApplicationPrivateClass::InstancesInfo *instance = static_cast(d->memory_->data()); + QElapsedTimer time; + time.start(); + + // Make sure the shared memory block is initialized and in a consistent state + while (d->blockChecksum() != instance->checksum) { + + // If more than 5 seconds have elapsed, assume the primary instance crashed and assume its position + if (time.elapsed() > 5000) { + qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5 seconds. 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 initialize faster + if (locked) { + if (d->memory_->unlock()) { + locked = false; + } + else { + qCritical() << "SingleApplication: Unable to unlock shared memory block for random wait:" << memory->error() << memory->errorString(); + return; + } + } + + SingleApplicationPrivateClass::randomSleep(); + + if (!d->memory_->lock()) { + qCritical() << "SingleApplication: Unable to lock shared memory block after random wait:" << memory->error() << memory->errorString(); + return; + } + locked = true; + + } + + if (instance->primary) { + // Check if another instance can be started + if (allowSecondary) { + d->startSecondary(); + if (d->options_ & Mode::SecondaryNotification) { + d->connectToPrimary(timeout, SingleApplicationPrivateClass::SecondaryInstance); + } + } + } + else { + d->startPrimary(); + primary = true; + } + + if (locked) { + if (d->memory_->unlock()) { + locked = false; + } + else { + qWarning() << "SingleApplication: Unable to unlock shared memory block:" << memory->error() << memory->errorString(); + } + } + + if (!primary && !allowSecondary) { + d->connectToPrimary(timeout, SingleApplicationPrivateClass::NewInstance); + } + +} + +SingleApplicationClass::~SingleApplicationClass() { + +#if defined(SINGLEAPPLICATION) + Q_D(SingleApplication); +#elif defined(SINGLECOREAPPLICATION) + Q_D(SingleCoreApplication); +#endif + + delete d; + +} + +/** + * Checks if the current application instance is primary. + * @return Returns true if the instance is primary, false otherwise. + */ +bool SingleApplicationClass::isPrimary() const { + +#if defined(SINGLEAPPLICATION) + Q_D(const SingleApplication); +#elif defined(SINGLECOREAPPLICATION) + Q_D(const SingleCoreApplication); +#endif + + return d->server_ != nullptr; + +} + +/** + * Checks if the current application instance is secondary. + * @return Returns true if the instance is secondary, false otherwise. + */ +bool SingleApplicationClass::isSecondary() const { + +#if defined(SINGLEAPPLICATION) + Q_D(const SingleApplication); +#elif defined(SINGLECOREAPPLICATION) + Q_D(const SingleCoreApplication); +#endif + + return d->server_ == nullptr; + +} + +/** + * 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 SingleApplicationClass::instanceId() const { + +#if defined(SINGLEAPPLICATION) + Q_D(const SingleApplication); +#elif defined(SINGLECOREAPPLICATION) + Q_D(const SingleCoreApplication); +#endif + + return d->instanceNumber_; + +} + +/** + * 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 SingleApplicationClass::primaryPid() const { + +#if defined(SINGLEAPPLICATION) + Q_D(const SingleApplication); +#elif defined(SINGLECOREAPPLICATION) + Q_D(const SingleCoreApplication); +#endif + + return d->primaryPid(); + +} + +/** + * Returns the username the primary instance is running as. + * @return Returns the username the primary instance is running as. + */ +QString SingleApplicationClass::primaryUser() const { + +#if defined(SINGLEAPPLICATION) + Q_D(const SingleApplication); +#elif defined(SINGLECOREAPPLICATION) + Q_D(const SingleCoreApplication); +#endif + + return d->primaryUser(); + +} + +/** + * Returns the username the current instance is running as. + * @return Returns the username the current instance is running as. + */ +QString SingleApplicationClass::currentUser() const { + return SingleApplicationPrivateClass::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 successfully, false otherwise. + */ +bool SingleApplicationClass::sendMessage(const QByteArray &message, const int timeout) { + +#if defined(SINGLEAPPLICATION) + Q_D(SingleApplication); +#elif defined(SINGLECOREAPPLICATION) + Q_D(SingleCoreApplication); +#endif + + // Nobody to connect to + if (isPrimary()) return false; + + // Make sure the socket is connected + if (!d->connectToPrimary(timeout, SingleApplicationPrivateClass::Reconnect)) { + return false; + } + + return d->writeConfirmedMessage(timeout, message); + +} diff --git a/3rdparty/singleapplication/singleapplication_t.h b/3rdparty/singleapplication/singleapplication_t.h new file mode 100644 index 00000000..f3bb3767 --- /dev/null +++ b/3rdparty/singleapplication/singleapplication_t.h @@ -0,0 +1,172 @@ +// The MIT License (MIT) +// +// 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 +// 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: +// +// 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. + +// +// W A R N I N G !!! +// ----------------- +// +// This is a modified version of SingleApplication, +// The original version is at: +// +// https://github.com/itay-grudev/SingleApplication +// +// + +#ifndef SINGLEAPPLICATION_T_H +#define SINGLEAPPLICATION_T_H + +#include + +#undef ApplicationClass +#undef SingleApplicationClass +#undef SingleApplicationPrivateClass + +#if defined(SINGLEAPPLICATION) +# include +# define ApplicationClass QApplication +# define SingleApplicationClass SingleApplication +# define SingleApplicationPrivateClass SingleApplicationPrivate +#elif defined(SINGLECOREAPPLICATION) +# include +# define ApplicationClass QCoreApplication +# define SingleApplicationClass SingleCoreApplication +# define SingleApplicationPrivateClass SingleCoreApplicationPrivate +#else +# error "Define SINGLEAPPLICATION or SINGLECOREAPPLICATION." +#endif + +#include +#include + +class SingleApplicationPrivateClass; + +/** + * @brief The SingleApplication class handles multiple instances of the same Application + * @see QApplication + */ +class SingleApplicationClass : public ApplicationClass { // clazy:exclude=ctor-missing-parent-argument + Q_OBJECT + + 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 class 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 + * initialization will be completed in given time, though is a good hint. + * Usually 4*timeout would be the worst case (fail) scenario. + */ + explicit SingleApplicationClass(int &argc, char *argv[], const bool allowSecondary = false, const Options options = Mode::User, const int timeout = 1000); + ~SingleApplicationClass() 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, const int timeout = 1000); + + signals: + void instanceStarted(); + void receivedMessage(quint32 instanceId, QByteArray message); + + private: + SingleApplicationPrivateClass *d_ptr; +#if defined(SINGLEAPPLICATION) + Q_DECLARE_PRIVATE(SingleApplication) +#elif defined(SINGLECOREAPPLICATION) + Q_DECLARE_PRIVATE(SingleCoreApplication) +#endif + void abortSafely(); +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplicationClass::Options) + +#endif // SINGLEAPPLICATION_T_H diff --git a/3rdparty/singleapplication/singlecoreapplication/CMakeLists.txt b/3rdparty/singleapplication/singlecoreapplication/CMakeLists.txt new file mode 100644 index 00000000..7c3f82d7 --- /dev/null +++ b/3rdparty/singleapplication/singlecoreapplication/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.7) + +add_definitions(-DSINGLECOREAPPLICATION) + +set(SOURCES ../singleapplication_t.cpp ../singleapplication_p.cpp) +set(HEADERS ../singleapplication_t.h ../singleapplication_p.h) +qt_wrap_cpp(MOC ${HEADERS}) +add_library(singlecoreapplication STATIC ${SOURCES} ${MOC}) +target_include_directories(singlecoreapplication PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_BINARY_DIR}/.. + ${Boost_INCLUDE_DIRS} +) +target_link_libraries(singlecoreapplication PUBLIC + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network +) diff --git a/3rdparty/singleapplication/singlecoreapplication/singlecoreapplication.h b/3rdparty/singleapplication/singlecoreapplication/singlecoreapplication.h new file mode 100644 index 00000000..0d510c61 --- /dev/null +++ b/3rdparty/singleapplication/singlecoreapplication/singlecoreapplication.h @@ -0,0 +1,13 @@ +#ifndef SINGLECOREAPPLICATION_H +#define SINGLECOREAPPLICATION_H + +#ifdef SINGLECOREAPPLICATION +# error "SINGLECOREAPPLICATION already defined." +#endif + +#define SINGLECOREAPPLICATION +#include "../singleapplication_t.h" +#undef SINGLEAPPLICATION_T_H +#undef SINGLECOREAPPLICATION + +#endif // SINGLECOREAPPLICATION_H diff --git a/CMakeLists.txt b/CMakeLists.txt index 07409145..7e866bd4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -294,28 +294,12 @@ if(NOT TAGLIB_FOUND AND NOT TAGPARSER_FOUND) endif() # SingleApplication -if(QT_VERSION_MAJOR EQUAL 5) - set(KDSINGLEAPPLICATION_NAME "KDSingleApplication") -else() - set(KDSINGLEAPPLICATION_NAME "KDSingleApplication-qt${QT_VERSION_MAJOR}") -endif() -find_package(${KDSINGLEAPPLICATION_NAME} 1.1.0) -if(TARGET KDAB::kdsingleapplication) - if(QT_VERSION_MAJOR EQUAL 5) - set(KDSINGLEAPPLICATION_VERSION "${KDSingleApplication_VERSION}") - elseif(QT_VERSION_MAJOR EQUAL 6) - set(KDSINGLEAPPLICATION_VERSION "${KDSingleApplication-qt6_VERSION}") - endif() - message(STATUS "Using system KDSingleApplication (Version ${KDSINGLEAPPLICATION_VERSION})") - set(SINGLEAPPLICATION_LIBRARIES KDAB::kdsingleapplication) -else() - message(STATUS "Using 3rdparty KDSingleApplication") - set(HAVE_KDSINGLEAPPLICATION_OPTIONS ON) - add_subdirectory(3rdparty/kdsingleapplication) - set(SINGLEAPPLICATION_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/kdsingleapplication/KDSingleApplication/src) - set(SINGLEAPPLICATION_LIBRARIES kdsingleapplication) - add_definitions(-DKDSINGLEAPPLICATION_STATIC_BUILD) -endif() +add_subdirectory(3rdparty/singleapplication) +set(SINGLEAPPLICATION_INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singleapplication + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singlecoreapplication +) +set(SINGLEAPPLICATION_LIBRARIES singleapplication singlecoreapplication) if(APPLE) add_subdirectory(3rdparty/SPMediaKeyTap) diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 0195f2c0..538f77cd 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -2404,6 +2404,13 @@ void MainWindow::CommandlineOptionsReceived(const QByteArray &string_options) { } +void MainWindow::CommandlineOptionsReceived(const quint32 instanceId, const QByteArray &string_options) { + + Q_UNUSED(instanceId); + CommandlineOptionsReceived(string_options); + +} + void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) { switch (options.player_action()) { diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 6053f92e..0269eaf3 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -272,6 +272,7 @@ class MainWindow : public QMainWindow, public PlatformInterface { public slots: void CommandlineOptionsReceived(const QByteArray &string_options); + void CommandlineOptionsReceived(const quint32 instanceId, const QByteArray &string_options); void Raise(); private: diff --git a/src/main.cpp b/src/main.cpp index 58e849a4..e64a1778 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,7 +72,8 @@ #include "utilities/envutils.h" -#include +#include +#include #ifdef HAVE_QTSPARKLE # if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) @@ -155,16 +156,15 @@ int main(int argc, char *argv[]) { { // Only start a core application now, so we can check if there's another instance without requiring an X server. // This MUST be done before parsing the commandline options so QTextCodec gets the right system locale for filenames. - QCoreApplication core_app(argc, argv); - KDSingleApplication single_app(QCoreApplication::applicationName(), KDSingleApplication::Option::IncludeUsernameInSocketName); + SingleCoreApplication core_app(argc, argv, true, SingleCoreApplication::Mode::User | SingleCoreApplication::Mode::ExcludeAppVersion | SingleCoreApplication::Mode::ExcludeAppPath); // Parse commandline options - need to do this before starting the full QApplication, so it works without an X server if (!options.Parse()) return 1; logging::SetLevels(options.log_levels()); - if (!single_app.isPrimaryInstance()) { + if (core_app.isSecondary()) { if (options.is_empty()) { qLog(Info) << "Strawberry is already running - activating existing window (1)"; } - if (!single_app.sendMessage(options.Serialize())) { + if (!core_app.sendMessage(options.Serialize(), 5000)) { qLog(Error) << "Could not send message to primary instance."; } return 0; @@ -192,13 +192,12 @@ int main(int argc, char *argv[]) { QGuiApplication::setDesktopFileName("org.strawberrymusicplayer.strawberry"); QGuiApplication::setQuitOnLastWindowClosed(false); - QApplication a(argc, argv); - KDSingleApplication single_app(QCoreApplication::applicationName(), KDSingleApplication::Option::IncludeUsernameInSocketName); - if (!single_app.isPrimaryInstance()) { + SingleApplication a(argc, argv, true, SingleApplication::Mode::User | SingleApplication::Mode::ExcludeAppVersion | SingleApplication::Mode::ExcludeAppPath); + if (a.isSecondary()) { if (options.is_empty()) { qLog(Info) << "Strawberry is already running - activating existing window (2)"; } - if (!single_app.sendMessage(options.Serialize())) { + if (!a.sendMessage(options.Serialize(), 5000)) { qLog(Error) << "Could not send message to primary instance."; } return 0; @@ -316,7 +315,7 @@ int main(int argc, char *argv[]) { #ifdef HAVE_DBUS QObject::connect(&mpris2, &mpris::Mpris2::RaiseMainWindow, &w, &MainWindow::Raise); #endif - QObject::connect(&single_app, &KDSingleApplication::messageReceived, &w, QOverload::of(&MainWindow::CommandlineOptionsReceived)); + QObject::connect(&a, &SingleApplication::receivedMessage, &w, QOverload::of(&MainWindow::CommandlineOptionsReceived)); int ret = QCoreApplication::exec();