diff options
Diffstat (limited to '3rd-party/SingleApplication')
-rw-r--r-- | 3rd-party/SingleApplication/CHANGELOG.md | 162 | ||||
-rw-r--r-- | 3rd-party/SingleApplication/CMakeLists.txt | 26 | ||||
-rw-r--r-- | 3rd-party/SingleApplication/LICENSE | 24 | ||||
-rw-r--r-- | 3rd-party/SingleApplication/README.md | 287 | ||||
-rw-r--r-- | 3rd-party/SingleApplication/Windows.md | 46 | ||||
-rw-r--r-- | 3rd-party/SingleApplication/singleapplication.cpp | 468 | ||||
-rw-r--r-- | 3rd-party/SingleApplication/singleapplication.h | 135 | ||||
-rw-r--r-- | 3rd-party/SingleApplication/singleapplication_p.h | 85 |
8 files changed, 1233 insertions, 0 deletions
diff --git a/3rd-party/SingleApplication/CHANGELOG.md b/3rd-party/SingleApplication/CHANGELOG.md new file mode 100644 index 0000000..4665222 --- /dev/null +++ b/3rd-party/SingleApplication/CHANGELOG.md @@ -0,0 +1,162 @@ +Changelog +========= + +__3.0.10__ +---------- + +* Removed C style casts and eliminated all clang warnings. Fixed `instanceId` + reading from only one byte in the message deserialization. Cleaned up + serialization code using `QDataStream`. Changed connection type to use + `quint8 enum` rather than `char`. +* Renamed `SingleAppConnectionType` to `ConnectionType`. Added initialization + values to all `ConnectionType` enum cases. + + _Jedidiah Buck McCready_ + +__3.0.9__ +--------- + +* Added SingleApplicationPrivate::primaryPid() as a solution to allow + bringing the primary window of an application to the foreground on + Windows. + + _Eelco van Dam from Peacs BV_ + +__3.0.8__ +--------- + +* Bug fix - changed QApplication::instance() to QCoreApplication::instance() + + _Evgeniy Bazhenov_ + +__3.0.7a__ +---------- + +* Fixed compilation error with Mingw32 in MXE thanks to Vitaly Tonkacheyev. +* Removed QMutex used for thread safe behaviour. The implementation now uses + QCoreApplication::instance() to get an instance to SingleApplication for + memory deallocation. + +__3.0.6a__ +---------- + +* Reverted GetUserName API usage on Windows. Fixed bug with missing library. +* Fixed bug in the Calculator example, preventing it's window to be raised + on Windows. + + Special thanks to Charles Gunawan. + +__3.0.5a__ +---------- + +* Fixed a memory leak in the SingleApplicationPrivate destructor. + + _Sergei Moiseev_ + +__3.0.4a__ +---------- + +* Fixed shadow and uninitialised variable warnings. + + _Paul Walmsley_ + +__3.0.3a__ +---------- + +* Removed Microsoft Windows specific code for getting username due to + multiple problems and compiler differences on Windows platforms. On + Windows the shared memory block in User mode now includes the user's + home path (which contains the user's username). + +* Explicitly getting absolute path of the user's home directory as on Unix + a relative path (`~`) may be returned. + +__3.0.2a__ +---------- + +* Fixed bug on Windows when username containing wide characters causes the + library to crash. + + _Le Liu_ + +__3.0.1a__ +---------- + +* Allows the application path and version to be excluded from the server name + hash. The following flags were added for this purpose: + * `SingleApplication::Mode::ExcludeAppVersion` + * `SingleApplication::Mode::ExcludeAppPath` +* Allow a non elevated process to connect to a local server created by an + elevated process run by the same user on Windows +* Fixes a problem with upper case letters in paths on Windows + + _Le Liu_ + +__v3.0a__ +--------- + +* Depricated secondary instances count. +* Added a sendMessage() method to send a message to the primary instance. +* Added a receivedMessage() signal, emitted when a message is received from a + secondary instance. +* The SingleApplication constructor's third parameter is now a bool + specifying if the current instance should be allowed to run as a secondary + instance if there is already a primary instance. +* The SingleApplication constructor accept a fourth parameter specifying if + the SingleApplication block should be User-wide or System-wide. +* SingleApplication no longer relies on `applicationName` and + `organizationName` to be set. It instead concatenates all of the following + data and computes a `SHA256` hash which is used as the key of the + `QSharedMemory` block and the `QLocalServer`. Since at least + `applicationFilePath` is always present there is no need to explicitly set + any of the following prior to initialising `SingleApplication`. + * `QCoreApplication::applicationName` + * `QCoreApplication::applicationVersion` + * `QCoreApplication::applicationFilePath` + * `QCoreApplication::organizationName` + * `QCoreApplication::organizationDomain` + * User name or home directory path if in User mode +* The primary instance is no longer notified when a secondary instance had + been started by default. A `Mode` flag for this feature exists. +* Added `instanceNumber()` which represents a unique identifier for each + secondary instance started. When called from the primary instance will + return `0`. + +__v2.4__ +-------- + +* Stability improvements +* Support for secondary instances. +* The library now recovers safely after the primary process has crashed +and the shared memory had not been deleted. + +__v2.3__ +-------- + +* Improved pimpl design and inheritance safety. + + _Vladislav Pyatnichenko_ + +__v2.2__ +-------- + +* The `QAPPLICATION_CLASS` macro can now be defined in the file including the +Single Application header or with a `DEFINES+=` statement in the project file. + +__v2.1__ +-------- + +* A race condition can no longer occur when starting two processes nearly + simultaneously. + + Fix issue [#3](https://github.com/itay-grudev/SingleApplication/issues/3) + +__v2.0__ +-------- + +* SingleApplication is now being passed a reference to `argc` instead of a + copy. + + Fix issue [#1](https://github.com/itay-grudev/SingleApplication/issues/1) + +* Improved documentation. diff --git a/3rd-party/SingleApplication/CMakeLists.txt b/3rd-party/SingleApplication/CMakeLists.txt new file mode 100644 index 0000000..5de0253 --- /dev/null +++ b/3rd-party/SingleApplication/CMakeLists.txt @@ -0,0 +1,26 @@ +# Find includes in corresponding build directories +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +# Instruct CMake to run moc automatically when needed. +set(CMAKE_AUTOMOC ON) +#set(CMAKE_AUTOUIC ON) +#set(CMAKE_AUTORCC ON) + +add_library(SingleApplication + singleapplication.cpp + singleapplication.h + singleapplication_p.h) + +target_link_libraries(SingleApplication + Qt5::Core Qt5::Network Qt5::Widgets +) + +target_compile_definitions(SingleApplication + PRIVATE QAPPLICATION_CLASS=QApplication +) + +if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + target_link_libraries(SingleApplication + Advapi32 + ) +endif() diff --git a/3rd-party/SingleApplication/LICENSE b/3rd-party/SingleApplication/LICENSE new file mode 100644 index 0000000..85b2a14 --- /dev/null +++ b/3rd-party/SingleApplication/LICENSE @@ -0,0 +1,24 @@ +The MIT License (MIT) + +Copyright (c) Itay Grudev 2015 - 2016 + +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/3rd-party/SingleApplication/README.md b/3rd-party/SingleApplication/README.md new file mode 100644 index 0000000..82e7a5b --- /dev/null +++ b/3rd-party/SingleApplication/README.md @@ -0,0 +1,287 @@ +SingleApplication +================= + +This is a replacement of the QtSingleApplication for `Qt5`. + +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. + +The library sets up a `QLocalServer` and a `QSharedMemory` block. The first +instance of your Application is your Primary Instance. It would check if the +shared memory block exists and if not it will start a `QLocalServer` and listen +for connections. Each subsequent instance of your application would check if the +shared memory block exists and if it does, it will connect to the QLocalServer +to notify the primary instance that a new instance had been started, after which +it would terminate with status code `0`. In the Primary Instance +`SingleApplication` would emit the `instanceStarted()` signal upon detecting +that a new instance had been started. + +The library uses `stdlib` to terminate the program with the `exit()` function. + +You can use the library as if you use any other `QCoreApplication` derived +class: + +```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 and include it's contents with a `.pri` file. Here is +how: + +```bash +git submodule add git@github.com:itay-grudev/SingleApplication.git singleapplication +``` + +Then include the `singleapplication.pri` file in your `.pro` project file. Also +don't forget to specify which `QCoreApplication` class your app is using if it +is not `QCoreApplication`. + +```qmake +include(singleapplication/singleapplication.pri) +DEFINES += QAPPLICATION_CLASS=QApplication +``` + +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.* + +API +--- + +### Members + +```cpp +SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 100 ) +``` + +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. + +*__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 + +--- + +```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. + +### 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. + +To handle an issue on `*nix` systems, where the operating system owns the shared +memory block and if the program crashes the memory remains untouched, the +library binds to the following signals and closes the program with +`error code = 128 + signum` where signum is the number representation of the +signal listed below. Handling the signal is required in order to safely delete +the `QSharedMemory` block. Each of these signals are potentially lethal and will +results in process termination. + +* `SIGHUP` - `1`, Hangup. +* `SIGINT` - `2`, Terminal interrupt signal +* `SIGQUIT` - `3`, Terminal quit signal. +* `SIGILL` - `4`, Illegal instruction. +* `SIGABRT` - `6`, Process abort signal. +* `SIGBUS` - `7`, Access to an undefined portion of a memory object. +* `SIGFPE` - `8`, Erroneous arithmetic operation (such as division by zero). +* `SIGSEGV` - `11`, Invalid memory reference. +* `SIGSYS` - `12`, Bad system call. +* `SIGPIPE` - `13`, Write on a pipe with no one to read it. +* `SIGALRM` - `14`, Alarm clock. +* `SIGTERM` - `15`, Termination signal. +* `SIGXCPU` - `24`, CPU time limit exceeded. +* `SIGXFSZ` - `25`, File size limit exceeded. + +Additionally the library can recover from being killed with uncatchable signals +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 some of the examples distributed +under the BSD license. diff --git a/3rd-party/SingleApplication/Windows.md b/3rd-party/SingleApplication/Windows.md new file mode 100644 index 0000000..48b0748 --- /dev/null +++ b/3rd-party/SingleApplication/Windows.md @@ -0,0 +1,46 @@ +Windows Specific Implementations +================================ + +Setting the foreground window +----------------------------- + +In the `instanceStarted()` example in the `README` we demonstrated how an +application can bring it's primary instance window whenever a second copy +of the application is started. + +On Windows the ability to bring the application windows to the foreground is +restricted, see [`AllowSetForegroundWindow()`][AllowSetForegroundWindow] for more +details. + +The background process (the primary instance) can bring its windows to the +foreground if it is allowed by the current foreground process (the secondary +instance). To bypass this `SingleApplication` must be initialized with the +`allowSecondary` parameter set to `true` and the `options` parameter must +include `Mode::SecondaryNotification`, See `SingleApplication::Mode` for more +details. + +Here is an example: + +```cpp +if( app.isSecondary() ) { + // This API requires LIBS += User32.lib to be added to the project + AllowSetForegroundWindow( DWORD( app.getPrimaryPid() ) ); +} + +if( app.isPrimary() ) { + QObject::connect( + &app, + &SingleApplication::instanceStarted, + this, + &App::instanceStarted + ); +} +``` + +```cpp +void App::instanceStarted() { + QApplication::setActiveWindow( [window/widget to set to the foreground] ); +} +``` + +[AllowSetForegroundWindow]: https://msdn.microsoft.com/en-us/library/windows/desktop/ms632668.aspx diff --git a/3rd-party/SingleApplication/singleapplication.cpp b/3rd-party/SingleApplication/singleapplication.cpp new file mode 100644 index 0000000..c74d6db --- /dev/null +++ b/3rd-party/SingleApplication/singleapplication.cpp @@ -0,0 +1,468 @@ +// The MIT License (MIT) +// +// Copyright (c) Itay Grudev 2015 - 2016 +// +// 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. + +#include <cstdlib> + +#include <QtCore/QDir> +#include <QtCore/QProcess> +#include <QtCore/QByteArray> +#include <QtCore/QSemaphore> +#include <QtCore/QSharedMemory> +#include <QtCore/QStandardPaths> +#include <QtCore/QCryptographicHash> +#include <QtCore/QDataStream> +#include <QtNetwork/QLocalServer> +#include <QtNetwork/QLocalSocket> + +#ifdef Q_OS_UNIX + #include <signal.h> + #include <unistd.h> +#endif + +#ifdef Q_OS_WIN + #include <windows.h> + #include <lmcons.h> +#endif + +#include "singleapplication.h" +#include "singleapplication_p.h" + + +SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr ) : q_ptr( q_ptr ) { + server = nullptr; + socket = nullptr; +} + +SingleApplicationPrivate::~SingleApplicationPrivate() +{ + if( socket != nullptr ) { + socket->close(); + delete socket; + } + + memory->lock(); + InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data()); + if( server != nullptr ) { + server->close(); + delete server; + inst->primary = false; + inst->primaryPid = -1; + } + memory->unlock(); + + delete memory; +} + +void SingleApplicationPrivate::genBlockServerName( int timeout ) +{ + QCryptographicHash appData( QCryptographicHash::Sha256 ); + appData.addData( "SingleApplication", 17 ); + appData.addData( SingleApplication::app_t::applicationName().toUtf8() ); + appData.addData( SingleApplication::app_t::organizationName().toUtf8() ); + appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() ); + + if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ) { + appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() ); + } + + if( ! (options & SingleApplication::Mode::ExcludeAppPath) ) { +#ifdef Q_OS_WIN + appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); +#else + appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); +#endif + } + + // User level block requires a user specific data in the hash + if( options & SingleApplication::Mode::User ) { +#ifdef Q_OS_WIN + Q_UNUSED(timeout); + wchar_t username [ UNLEN + 1 ]; + // Specifies size of the buffer on input + DWORD usernameLength = UNLEN + 1; + if( GetUserNameW( username, &usernameLength ) ) { + appData.addData( QString::fromWCharArray(username).toUtf8() ); + } else { + appData.addData( QStandardPaths::standardLocations( QStandardPaths::HomeLocation ).join("").toUtf8() ); + } +#endif +#ifdef Q_OS_UNIX + QProcess process; + process.start( "whoami" ); + if( process.waitForFinished( timeout ) && + process.exitCode() == QProcess::NormalExit) { + appData.addData( process.readLine() ); + } else { + appData.addData( + QDir( + QStandardPaths::standardLocations( QStandardPaths::HomeLocation ).first() + ).absolutePath().toUtf8() + ); + } +#endif + } + + // Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with + // server naming requirements. + blockServerName = appData.result().toBase64().replace("/", "_"); +} + +void SingleApplicationPrivate::startPrimary( bool resetMemory ) +{ + Q_Q(SingleApplication); + +#ifdef Q_OS_UNIX + // Handle any further termination signals to ensure the + // QSharedMemory block is deleted even if the process crashes + crashHandler(); +#endif + // Successful creation means that no main process exists + // So we start a QLocalServer to listen for connections + QLocalServer::removeServer( blockServerName ); + server = new QLocalServer(); + + // Restrict access to the socket according to the + // SingleApplication::Mode::User flag on User level or no restrictions + if( options & SingleApplication::Mode::User ) { + server->setSocketOptions( QLocalServer::UserAccessOption ); + } else { + server->setSocketOptions( QLocalServer::WorldAccessOption ); + } + + server->listen( blockServerName ); + QObject::connect( + server, + &QLocalServer::newConnection, + this, + &SingleApplicationPrivate::slotConnectionEstablished + ); + + // Reset the number of connections + memory->lock(); + InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data()); + + if( resetMemory ) { + inst->secondary = 0; + } + + inst->primary = true; + inst->primaryPid = q->applicationPid(); + + memory->unlock(); + + instanceNumber = 0; +} + +void SingleApplicationPrivate::startSecondary() +{ +#ifdef Q_OS_UNIX + // Handle any further termination signals to ensure the + // QSharedMemory block is deleted even if the process crashes + crashHandler(); +#endif +} + +void SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType ) +{ + // Connect to the Local Server of the Primary Instance if not already + // connected. + if( socket == nullptr ) { + socket = new QLocalSocket(); + } + + // If already connected - we are done; + if( socket->state() == QLocalSocket::ConnectedState ) + return; + + // If not connect + if( socket->state() == QLocalSocket::UnconnectedState || + socket->state() == QLocalSocket::ClosingState ) { + socket->connectToServer( blockServerName ); + } + + // Wait for being connected + if( socket->state() == QLocalSocket::ConnectingState ) { + socket->waitForConnected( msecs ); + } + + // Initialisation message according to the SingleApplication protocol + if( socket->state() == QLocalSocket::ConnectedState ) { + // Notify the parent that a new instance had been started; + QByteArray initMsg; + QDataStream writeStream(&initMsg, QIODevice::WriteOnly); + writeStream.setVersion(QDataStream::Qt_5_6); + writeStream << blockServerName.toLatin1(); + writeStream << static_cast<quint8>(connectionType); + writeStream << instanceNumber; + quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length())); + writeStream << checksum; + + socket->write( initMsg ); + socket->flush(); + socket->waitForBytesWritten( msecs ); + } +} + +qint64 SingleApplicationPrivate::primaryPid() +{ + qint64 pid; + + memory->lock(); + InstancesInfo* inst = static_cast<InstancesInfo*>(memory->data()); + pid = inst->primaryPid; + memory->unlock(); + + return pid; +} + +#ifdef Q_OS_UNIX + void SingleApplicationPrivate::crashHandler() + { + // Handle any further termination signals to ensure the + // QSharedMemory block is deleted even if the process crashes + signal( SIGHUP, SingleApplicationPrivate::terminate ); // 1 + signal( SIGINT, SingleApplicationPrivate::terminate ); // 2 + signal( SIGQUIT, SingleApplicationPrivate::terminate ); // 3 + signal( SIGILL, SingleApplicationPrivate::terminate ); // 4 + signal( SIGABRT, SingleApplicationPrivate::terminate ); // 6 + signal( SIGFPE, SingleApplicationPrivate::terminate ); // 8 + signal( SIGBUS, SingleApplicationPrivate::terminate ); // 10 + signal( SIGSEGV, SingleApplicationPrivate::terminate ); // 11 + signal( SIGSYS, SingleApplicationPrivate::terminate ); // 12 + signal( SIGPIPE, SingleApplicationPrivate::terminate ); // 13 + signal( SIGALRM, SingleApplicationPrivate::terminate ); // 14 + signal( SIGTERM, SingleApplicationPrivate::terminate ); // 15 + signal( SIGXCPU, SingleApplicationPrivate::terminate ); // 24 + signal( SIGXFSZ, SingleApplicationPrivate::terminate ); // 25 + } + + void SingleApplicationPrivate::terminate( int signum ) + { + delete ((SingleApplication*)QCoreApplication::instance())->d_ptr; + ::exit( 128 + signum ); + } +#endif + +/** + * @brief Executed when a connection has been made to the LocalServer + */ +void SingleApplicationPrivate::slotConnectionEstablished() +{ + Q_Q(SingleApplication); + + QLocalSocket *nextConnSocket = server->nextPendingConnection(); + + quint32 instanceId = 0; + ConnectionType connectionType = InvalidConnection; + if( nextConnSocket->waitForReadyRead( 100 ) ) { + // read all data from message in same order/format as written + QByteArray msgBytes = nextConnSocket->read(nextConnSocket->bytesAvailable() - static_cast<qint64>(sizeof(quint16))); + QByteArray checksumBytes = nextConnSocket->read(sizeof(quint16)); + QDataStream readStream(msgBytes); + readStream.setVersion(QDataStream::Qt_5_6); + + // server name + QByteArray latin1Name; + readStream >> latin1Name; + // connectioon type + quint8 connType = InvalidConnection; + readStream >> connType; + connectionType = static_cast<ConnectionType>(connType); + // instance id + readStream >> instanceId; + // checksum + quint16 msgChecksum = 0; + QDataStream checksumStream(checksumBytes); + checksumStream.setVersion(QDataStream::Qt_5_6); + checksumStream >> msgChecksum; + + const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length())); + + if (readStream.status() != QDataStream::Ok || QLatin1String(latin1Name) != blockServerName || msgChecksum != actualChecksum) { + connectionType = InvalidConnection; + } + } + + if( connectionType == InvalidConnection ) { + nextConnSocket->close(); + delete nextConnSocket; + return; + } + + QObject::connect( + nextConnSocket, + &QLocalSocket::aboutToClose, + this, + [nextConnSocket, instanceId, this]() { + Q_EMIT this->slotClientConnectionClosed( nextConnSocket, instanceId ); + } + ); + + QObject::connect( + nextConnSocket, + &QLocalSocket::readyRead, + this, + [nextConnSocket, instanceId, this]() { + Q_EMIT this->slotDataAvailable( nextConnSocket, instanceId ); + } + ); + + if( connectionType == NewInstance || ( + connectionType == SecondaryInstance && + options & SingleApplication::Mode::SecondaryNotification + ) + ) { + Q_EMIT q->instanceStarted(); + } + + if( nextConnSocket->bytesAvailable() > 0 ) { + Q_EMIT this->slotDataAvailable( nextConnSocket, instanceId ); + } +} + +void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId ) +{ + Q_Q(SingleApplication); + Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() ); +} + +void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId ) +{ + if( closedSocket->bytesAvailable() > 0 ) + Q_EMIT slotDataAvailable( closedSocket, instanceId ); + closedSocket->deleteLater(); +} + +/** + * @brief Constructor. Checks and fires up LocalServer or closes the program + * if another instance already exists + * @param argc + * @param argv + * @param {bool} allowSecondaryInstances + */ +SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout ) + : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) +{ + Q_D(SingleApplication); + + // 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( timeout ); + + // Guarantee thread safe behaviour with a shared memory block. Also by + // explicitly attaching it and then deleting it we make sure that the + // memory is deleted even if the process had crashed on Unix. +#ifdef Q_OS_UNIX + d->memory = new QSharedMemory( d->blockServerName ); + d->memory->attach(); + delete d->memory; +#endif + d->memory = new QSharedMemory( d->blockServerName ); + + // Create a shared memory block + if( d->memory->create( sizeof( InstancesInfo ) ) ) { + d->startPrimary( true ); + return; + } else { + // Attempt to attach to the memory segment + if( d->memory->attach() ) { + d->memory->lock(); + InstancesInfo* inst = static_cast<InstancesInfo*>(d->memory->data()); + + if( ! inst->primary ) { + d->startPrimary( false ); + d->memory->unlock(); + return; + } + + // Check if another instance can be started + if( allowSecondary ) { + inst->secondary += 1; + d->instanceNumber = inst->secondary; + d->startSecondary(); + if( d->options & Mode::SecondaryNotification ) { + d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance ); + } + d->memory->unlock(); + return; + } + + d->memory->unlock(); + } + } + + d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); + delete d; + ::exit( EXIT_SUCCESS ); +} + +/** + * @brief Destructor + */ +SingleApplication::~SingleApplication() +{ + Q_D(SingleApplication); + delete d; +} + +bool SingleApplication::isPrimary() +{ + Q_D(SingleApplication); + return d->server != nullptr; +} + +bool SingleApplication::isSecondary() +{ + Q_D(SingleApplication); + return d->server == nullptr; +} + +quint32 SingleApplication::instanceId() +{ + Q_D(SingleApplication); + return d->instanceNumber; +} + +qint64 SingleApplication::primaryPid() +{ + Q_D(SingleApplication); + return d->primaryPid(); +} + +bool SingleApplication::sendMessage( QByteArray message, int timeout ) +{ + Q_D(SingleApplication); + + // Nobody to connect to + if( isPrimary() ) return false; + + // Make sure the socket is connected + d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ); + + d->socket->write( message ); + bool dataWritten = d->socket->flush(); + d->socket->waitForBytesWritten( timeout ); + return dataWritten; +} diff --git a/3rd-party/SingleApplication/singleapplication.h b/3rd-party/SingleApplication/singleapplication.h new file mode 100644 index 0000000..33a9898 --- /dev/null +++ b/3rd-party/SingleApplication/singleapplication.h @@ -0,0 +1,135 @@ +// The MIT License (MIT) +// +// Copyright (c) Itay Grudev 2015 - 2016 +// +// 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. + +#ifndef SINGLE_APPLICATION_H +#define SINGLE_APPLICATION_H + +#include <QtCore/QtGlobal> +#include <QtNetwork/QLocalSocket> + +#ifndef QAPPLICATION_CLASS + #define QAPPLICATION_CLASS QCoreApplication +#endif + +#include QT_STRINGIFY(QAPPLICATION_CLASS) + +class SingleApplicationPrivate; + +/** + * @brief The SingleApplication class handles multipe instances of the same + * Application + * @see QCoreApplication + */ +class SingleApplication : public QAPPLICATION_CLASS +{ + Q_OBJECT + + typedef QAPPLICATION_CLASS app_t; + +public: + /** + * @brief Mode of operation of SingleApplication. + * Whether the block should be user-wide or system-wide and whether the + * primary instance should be notified when a secondary instance had been + * started. + * @note Operating system can restrict the shared memory blocks to the same + * user, in which case the User/System modes will have no effect and the + * block will be user wide. + * @enum + */ + enum Mode { + User = 1 << 0, + System = 1 << 1, + SecondaryNotification = 1 << 2, + ExcludeAppVersion = 1 << 3, + ExcludeAppPath = 1 << 4 + }; + Q_DECLARE_FLAGS(Options, Mode) + + /** + * @brief Intitializes a SingleApplication instance with argc command line + * arguments in argv + * @arg {int &} argc - Number of arguments in argv + * @arg {const char *[]} argv - Supplied command line arguments + * @arg {bool} allowSecondary - Whether to start the instance as secondary + * if there is already a primary instance. + * @arg {Mode} mode - Whether for the SingleApplication block to be applied + * User wide or System wide. + * @arg {int} timeout - Timeout to wait in miliseconds. + * @note argc and argv may be changed as Qt removes arguments that it + * recognizes + * @note Mode::SecondaryNotification only works if set on both the primary + * instance and the secondary instance. + * @note The timeout is just a hint for the maximum time of blocking + * operations. It does not guarantee that the SingleApplication + * initialisation will be completed in given time, though is a good hint. + * Usually 4*timeout would be the worst case (fail) scenario. + * @see See the corresponding QAPPLICATION_CLASS constructor for reference + */ + explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 100 ); + ~SingleApplication(); + + /** + * @brief Returns if the instance is the primary instance + * @returns {bool} + */ + bool isPrimary(); + + /** + * @brief Returns if the instance is a secondary instance + * @returns {bool} + */ + bool isSecondary(); + + /** + * @brief Returns a unique identifier for the current instance + * @returns {qint32} + */ + quint32 instanceId(); + + /** + * @brief Returns the process ID (PID) of the primary instance + * @returns {qint64} + */ + qint64 primaryPid(); + + /** + * @brief Sends a message to the primary instance. Returns true on success. + * @param {int} timeout - Timeout for connecting + * @returns {bool} + * @note sendMessage() will return false if invoked from the primary + * instance. + */ + bool sendMessage( QByteArray message, int timeout = 100 ); + +Q_SIGNALS: + void instanceStarted(); + void receivedMessage( quint32 instanceId, QByteArray message ); + +private: + SingleApplicationPrivate *d_ptr; + Q_DECLARE_PRIVATE(SingleApplication) +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options) + +#endif // SINGLE_APPLICATION_H diff --git a/3rd-party/SingleApplication/singleapplication_p.h b/3rd-party/SingleApplication/singleapplication_p.h new file mode 100644 index 0000000..a990a53 --- /dev/null +++ b/3rd-party/SingleApplication/singleapplication_p.h @@ -0,0 +1,85 @@ +// The MIT License (MIT) +// +// Copyright (c) Itay Grudev 2015 - 2016 +// +// 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 file is not part of the SingleApplication API. It is used purely as an +// implementation detail. This header file may change from version to +// version without notice, or may even be removed. +// + +#ifndef SINGLEAPPLICATION_P_H +#define SINGLEAPPLICATION_P_H + +#include <QtCore/QSharedMemory> +#include <QtNetwork/QLocalServer> +#include <QtNetwork/QLocalSocket> +#include "singleapplication.h" + +struct InstancesInfo { + bool primary; + quint32 secondary; + qint64 primaryPid; +}; + +class SingleApplicationPrivate : public QObject { +Q_OBJECT +public: + enum ConnectionType : quint8 { + InvalidConnection = 0, + NewInstance = 1, + SecondaryInstance = 2, + Reconnect = 3 + }; + Q_DECLARE_PUBLIC(SingleApplication) + + SingleApplicationPrivate( SingleApplication *q_ptr ); + ~SingleApplicationPrivate(); + + void genBlockServerName( int msecs ); + void startPrimary( bool resetMemory ); + void startSecondary(); + void connectToPrimary(int msecs, ConnectionType connectionType ); + qint64 primaryPid(); + +#ifdef Q_OS_UNIX + void crashHandler(); + static void terminate( int signum ); +#endif + + QSharedMemory *memory; + SingleApplication *q_ptr; + QLocalSocket *socket; + QLocalServer *server; + quint32 instanceNumber; + QString blockServerName; + SingleApplication::Options options; + +public Q_SLOTS: + void slotConnectionEstablished(); + void slotDataAvailable( QLocalSocket*, quint32 ); + void slotClientConnectionClosed( QLocalSocket*, quint32 ); +}; + +#endif // SINGLEAPPLICATION_P_H |