Use SingleApplication
This commit is contained in:
parent
c52fc90306
commit
d6be8ee519
|
@ -1,4 +0,0 @@
|
||||||
[submodule "3rdparty/kdsingleapplication/KDSingleApplication"]
|
|
||||||
path = 3rdparty/kdsingleapplication/KDSingleApplication
|
|
||||||
url = https://github.com/KDAB/KDSingleApplication.git
|
|
||||||
branch = master
|
|
|
@ -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)
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit ffce501f4533bb8c19e9a89d712077acecd9c78d
|
|
|
@ -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)
|
|
@ -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.
|
|
@ -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 <QApplication>
|
||||||
|
#include <SingleApplication.h>
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,2 @@
|
||||||
|
#cmakedefine HAVE_GETEUID
|
||||||
|
#cmakedefine HAVE_GETPWUID
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
|
@ -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 <QtGlobal>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#ifdef Q_OS_UNIX
|
||||||
|
# include <unistd.h>
|
||||||
|
# include <sys/types.h>
|
||||||
|
# include <pwd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
# ifndef NOMINMAX
|
||||||
|
# define NOMINMAX 1
|
||||||
|
# endif
|
||||||
|
# include <windows.h>
|
||||||
|
# include <lmcons.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QThread>
|
||||||
|
#include <QIODevice>
|
||||||
|
#include <QSharedMemory>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QLocalServer>
|
||||||
|
#include <QLocalSocket>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
|
||||||
|
#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<InstancesInfo*>(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<InstancesInfo*>(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<InstancesInfo*>(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<InstancesInfo*>(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<int>(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<quint8>(connectionType);
|
||||||
|
writeStream << instanceNumber_;
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
|
||||||
|
#else
|
||||||
|
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
writeStream << checksum;
|
||||||
|
|
||||||
|
return writeConfirmedMessage(static_cast<int>(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<quint64>(msg.length());
|
||||||
|
|
||||||
|
if (!writeConfirmedFrame(static_cast<int>(timeout - time.elapsed()), header)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame 2: The message
|
||||||
|
return writeConfirmedFrame(static_cast<int>(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<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum)));
|
||||||
|
#else
|
||||||
|
quint16 checksum = qChecksum(static_cast<const char*>(memory_->constData()), offsetof(InstancesInfo, checksum));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return checksum;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 SingleApplicationPrivateClass::primaryPid() const {
|
||||||
|
|
||||||
|
memory_->lock();
|
||||||
|
InstancesInfo *instance = static_cast<InstancesInfo*>(memory_->data());
|
||||||
|
qint64 pid = instance->primaryPid;
|
||||||
|
memory_->unlock();
|
||||||
|
|
||||||
|
return pid;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SingleApplicationPrivateClass::primaryUser() const {
|
||||||
|
|
||||||
|
memory_->lock();
|
||||||
|
InstancesInfo *instance = static_cast<InstancesInfo*>(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<qint64>(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<qint64>(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<ConnectionType>(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<quint32>(msgBytes.length() - sizeof(quint16))));
|
||||||
|
#else
|
||||||
|
const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(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));
|
||||||
|
|
||||||
|
}
|
|
@ -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 <QtGlobal>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QHash>
|
||||||
|
|
||||||
|
#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<QLocalSocket*, ConnectionInfo> connectionMap_;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void slotConnectionEstablished();
|
||||||
|
void slotDataAvailable(QLocalSocket*, const quint32);
|
||||||
|
void slotClientConnectionClosed(QLocalSocket*, const quint32);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SINGLEAPPLICATION_P_H
|
|
@ -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 <cstdlib>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <boost/scope_exit.hpp>
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
#include <QThread>
|
||||||
|
#include <QSharedMemory>
|
||||||
|
#include <QLocalSocket>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QtDebug>
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
||||||
|
# include <QNativeIpcKey>
|
||||||
|
#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<QSharedMemory> memory = std::make_unique<QSharedMemory>(QNativeIpcKey(d->blockServerName_));
|
||||||
|
# else
|
||||||
|
std::unique_ptr<QSharedMemory> memory = std::make_unique<QSharedMemory>(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<SingleApplicationPrivateClass::InstancesInfo*>(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);
|
||||||
|
|
||||||
|
}
|
|
@ -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 <QtGlobal>
|
||||||
|
|
||||||
|
#undef ApplicationClass
|
||||||
|
#undef SingleApplicationClass
|
||||||
|
#undef SingleApplicationPrivateClass
|
||||||
|
|
||||||
|
#if defined(SINGLEAPPLICATION)
|
||||||
|
# include <QApplication>
|
||||||
|
# define ApplicationClass QApplication
|
||||||
|
# define SingleApplicationClass SingleApplication
|
||||||
|
# define SingleApplicationPrivateClass SingleApplicationPrivate
|
||||||
|
#elif defined(SINGLECOREAPPLICATION)
|
||||||
|
# include <QCoreApplication>
|
||||||
|
# define ApplicationClass QCoreApplication
|
||||||
|
# define SingleApplicationClass SingleCoreApplication
|
||||||
|
# define SingleApplicationPrivateClass SingleCoreApplicationPrivate
|
||||||
|
#else
|
||||||
|
# error "Define SINGLEAPPLICATION or SINGLECOREAPPLICATION."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <QFlags>
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
|
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
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
|
@ -300,33 +300,12 @@ if(NOT TAGLIB_FOUND AND NOT TAGPARSER_FOUND)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# SingleApplication
|
# SingleApplication
|
||||||
if(QT_VERSION_MAJOR EQUAL 5)
|
add_subdirectory(3rdparty/singleapplication)
|
||||||
set(KDSINGLEAPPLICATION_NAME "KDSingleApplication")
|
set(SINGLEAPPLICATION_INCLUDE_DIRS
|
||||||
else()
|
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singleapplication
|
||||||
set(KDSINGLEAPPLICATION_NAME "KDSingleApplication-qt${QT_VERSION_MAJOR}")
|
${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/singleapplication/singlecoreapplication
|
||||||
endif()
|
)
|
||||||
find_package(${KDSINGLEAPPLICATION_NAME})
|
set(SINGLEAPPLICATION_LIBRARIES singleapplication singlecoreapplication)
|
||||||
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()
|
|
||||||
if(KDSINGLEAPPLICATION_VERSION VERSION_GREATER_EQUAL 1.0.95)
|
|
||||||
set(HAVE_KDSINGLEAPPLICATION_OPTIONS ON)
|
|
||||||
else()
|
|
||||||
set(HAVE_KDSINGLEAPPLICATION_OPTIONS OFF)
|
|
||||||
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()
|
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
add_subdirectory(3rdparty/SPMediaKeyTap)
|
add_subdirectory(3rdparty/SPMediaKeyTap)
|
||||||
|
|
|
@ -2364,6 +2364,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) {
|
void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) {
|
||||||
|
|
||||||
switch (options.player_action()) {
|
switch (options.player_action()) {
|
||||||
|
|
|
@ -272,6 +272,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void CommandlineOptionsReceived(const QByteArray &string_options);
|
void CommandlineOptionsReceived(const QByteArray &string_options);
|
||||||
|
void CommandlineOptionsReceived(const quint32 instanceId, const QByteArray &string_options);
|
||||||
void Raise();
|
void Raise();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
27
src/main.cpp
27
src/main.cpp
|
@ -72,7 +72,8 @@
|
||||||
|
|
||||||
#include "utilities/envutils.h"
|
#include "utilities/envutils.h"
|
||||||
|
|
||||||
#include <kdsingleapplication.h>
|
#include <singleapplication.h>
|
||||||
|
#include <singlecoreapplication.h>
|
||||||
|
|
||||||
#ifdef HAVE_QTSPARKLE
|
#ifdef HAVE_QTSPARKLE
|
||||||
# if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
# if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||||
|
@ -155,20 +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.
|
// 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.
|
// This MUST be done before parsing the commandline options so QTextCodec gets the right system locale for filenames.
|
||||||
QCoreApplication core_app(argc, argv);
|
SingleCoreApplication core_app(argc, argv, true, SingleCoreApplication::Mode::User | SingleCoreApplication::Mode::ExcludeAppVersion | SingleCoreApplication::Mode::ExcludeAppPath);
|
||||||
#ifdef HAVE_KDSINGLEAPPLICATION_OPTIONS
|
|
||||||
KDSingleApplication single_app(QCoreApplication::applicationName(), KDSingleApplication::Option::IncludeUsernameInSocketName);
|
|
||||||
#else
|
|
||||||
KDSingleApplication single_app(QCoreApplication::applicationName());
|
|
||||||
#endif
|
|
||||||
// Parse commandline options - need to do this before starting the full QApplication, so it works without an X server
|
// Parse commandline options - need to do this before starting the full QApplication, so it works without an X server
|
||||||
if (!options.Parse()) return 1;
|
if (!options.Parse()) return 1;
|
||||||
logging::SetLevels(options.log_levels());
|
logging::SetLevels(options.log_levels());
|
||||||
if (!single_app.isPrimaryInstance()) {
|
if (core_app.isSecondary()) {
|
||||||
if (options.is_empty()) {
|
if (options.is_empty()) {
|
||||||
qLog(Info) << "Strawberry is already running - activating existing window (1)";
|
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.";
|
qLog(Error) << "Could not send message to primary instance.";
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -196,17 +192,12 @@ int main(int argc, char *argv[]) {
|
||||||
QGuiApplication::setDesktopFileName("org.strawberrymusicplayer.strawberry");
|
QGuiApplication::setDesktopFileName("org.strawberrymusicplayer.strawberry");
|
||||||
QGuiApplication::setQuitOnLastWindowClosed(false);
|
QGuiApplication::setQuitOnLastWindowClosed(false);
|
||||||
|
|
||||||
QApplication a(argc, argv);
|
SingleApplication a(argc, argv, true, SingleApplication::Mode::User | SingleApplication::Mode::ExcludeAppVersion | SingleApplication::Mode::ExcludeAppPath);
|
||||||
#ifdef HAVE_KDSINGLEAPPLICATION_OPTIONS
|
if (a.isSecondary()) {
|
||||||
KDSingleApplication single_app(QCoreApplication::applicationName(), KDSingleApplication::Option::IncludeUsernameInSocketName);
|
|
||||||
#else
|
|
||||||
KDSingleApplication single_app(QCoreApplication::applicationName());
|
|
||||||
#endif
|
|
||||||
if (!single_app.isPrimaryInstance()) {
|
|
||||||
if (options.is_empty()) {
|
if (options.is_empty()) {
|
||||||
qLog(Info) << "Strawberry is already running - activating existing window (2)";
|
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.";
|
qLog(Error) << "Could not send message to primary instance.";
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -324,7 +315,7 @@ int main(int argc, char *argv[]) {
|
||||||
#ifdef HAVE_DBUS
|
#ifdef HAVE_DBUS
|
||||||
QObject::connect(&mpris2, &mpris::Mpris2::RaiseMainWindow, &w, &MainWindow::Raise);
|
QObject::connect(&mpris2, &mpris::Mpris2::RaiseMainWindow, &w, &MainWindow::Raise);
|
||||||
#endif
|
#endif
|
||||||
QObject::connect(&single_app, &KDSingleApplication::messageReceived, &w, QOverload<const QByteArray&>::of(&MainWindow::CommandlineOptionsReceived));
|
QObject::connect(&a, &SingleApplication::receivedMessage, &w, QOverload<quint32, const QByteArray&>::of(&MainWindow::CommandlineOptionsReceived));
|
||||||
|
|
||||||
int ret = QCoreApplication::exec();
|
int ret = QCoreApplication::exec();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue