diff --git a/linphone-desktop/CMakeLists.txt b/linphone-desktop/CMakeLists.txt index 06b1b8b01..fc4acde5d 100644 --- a/linphone-desktop/CMakeLists.txt +++ b/linphone-desktop/CMakeLists.txt @@ -93,6 +93,7 @@ set(SOURCES src/components/sip-addresses/SipAddressesModel.cpp src/components/smart-search-bar/SmartSearchBarModel.cpp src/components/timeline/TimelineModel.cpp + src/externals/single-application/SingleApplication.cpp src/main.cpp ) @@ -123,6 +124,8 @@ set(HEADERS src/components/sip-addresses/SipAddressesModel.hpp src/components/smart-search-bar/SmartSearchBarModel.hpp src/components/timeline/TimelineModel.hpp + src/externals/single-application/SingleApplication.hpp + src/externals/single-application/SingleApplicationPrivate.hpp src/utils.hpp ) diff --git a/linphone-desktop/src/externals/single-application/SingleApplication.cpp b/linphone-desktop/src/externals/single-application/SingleApplication.cpp new file mode 100644 index 000000000..465c68747 --- /dev/null +++ b/linphone-desktop/src/externals/single-application/SingleApplication.cpp @@ -0,0 +1,452 @@ +// 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_UNIX + #include + #include +#endif // ifdef Q_OS_UNIX + +#ifdef Q_OS_WIN + #include + #include +#endif // ifdef Q_OS_WIN + +#include "SingleApplication.hpp" +#include "SingleApplicationPrivate.hpp" + +// ============================================================================= + +static const char NewInstance = 'N'; +static const char SecondaryInstance = 'S'; +static const char Reconnect = 'R'; +static const char InvalidConnection = '\0'; + +// ----------------------------------------------------------------------------- + +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 = (InstancesInfo *)memory->data(); + if (server != nullptr) { + server->close(); + delete server; + inst->primary = false; + } + 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 // ifdef Q_OS_WIN + } + + // 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_WIN + #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 // ifdef Q_OS_UNIX + } + + // 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) { + #ifdef Q_OS_UNIX + // Handle any further termination signals to ensure the + // QSharedMemory block is deleted even if the process crashes + crashHandler(); + #endif // ifdef Q_OS_UNIX + // 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 = (InstancesInfo *)memory->data(); + + if (resetMemory) { + inst->primary = true; + inst->secondary = 0; + } else { + inst->primary = true; + } + + 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 // ifdef Q_OS_UNIX +} + +void SingleApplicationPrivate::connectToPrimary (int msecs, char 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 = blockServerName.toLatin1(); + + initMsg.append(connectionType); + initMsg.append((const char *)&instanceNumber, sizeof(quint32)); + initMsg.append(QByteArray::number(qChecksum(initMsg.constData(), initMsg.length()), 256)); + + socket->write(initMsg); + socket->flush(); + socket->waitForBytesWritten(msecs); + } +} + +#ifdef Q_OS_UNIX + void SingleApplicationPrivate::crashHandler () { + // This guarantees the program will work even with multiple + // instances of SingleApplication in different threads. + // Which in my opinion is idiotic, but lets handle that too. + { + sharedMemMutex.lock(); + sharedMem.append(this); + sharedMemMutex.unlock(); + } + + // 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) { + while (!sharedMem.empty()) { + delete sharedMem.back(); + sharedMem.pop_back(); + } + ::exit(128 + signum); + } + + QList SingleApplicationPrivate::sharedMem; + QMutex SingleApplicationPrivate::sharedMemMutex; +#endif // ifdef Q_OS_UNIX + +/** + * @brief Executed when a connection has been made to the LocalServer + */ +void SingleApplicationPrivate::slotConnectionEstablished () { + Q_Q(SingleApplication); + + QLocalSocket *nextConnSocket = server->nextPendingConnection(); + + // Verify that the new connection follows the SingleApplication protocol + char connectionType = InvalidConnection; + quint32 instanceId; + QByteArray initMsg, tmp; + if (nextConnSocket->waitForReadyRead(100)) { + tmp = nextConnSocket->read(blockServerName.length()); + // Verify that the socket data start with blockServerName + if (tmp == blockServerName.toLatin1()) { + initMsg = tmp; + connectionType = nextConnSocket->read(1)[0]; + + switch (connectionType) { + case NewInstance: + case SecondaryInstance: + case Reconnect: { + initMsg += connectionType; + tmp = nextConnSocket->read(sizeof(quint32)); + const char *data = tmp.constData(); + instanceId = (quint32) * data; + initMsg += tmp; + // Verify the checksum of the initMsg + QByteArray checksum = QByteArray::number( + qChecksum(initMsg.constData(), initMsg.length()), + 256 + ); + tmp = nextConnSocket->read(checksum.length()); + if (checksum == tmp) + break; // Otherwise set to invalid connection (next line) + } + default: + 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 // ifdef Q_OS_UNIX + 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 = (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, SecondaryInstance); + } + d->memory->unlock(); + return; + } + + d->memory->unlock(); + } + } + + d->connectToPrimary(timeout, 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; +} + +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, Reconnect); + + d->socket->write(message); + bool dataWritten = d->socket->flush(); + d->socket->waitForBytesWritten(timeout); + return dataWritten; +} diff --git a/linphone-desktop/src/externals/single-application/SingleApplication.hpp b/linphone-desktop/src/externals/single-application/SingleApplication.hpp new file mode 100644 index 000000000..23b043d41 --- /dev/null +++ b/linphone-desktop/src/externals/single-application/SingleApplication.hpp @@ -0,0 +1,133 @@ +// 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. + +// See: https://github.com/itay-grudev/SingleApplication/ + +#ifndef SINGLE_APPLICATION_H +#define SINGLE_APPLICATION_H + +#include +#include + +#ifndef QAPPLICATION_CLASS + #define QAPPLICATION_CLASS QCoreApplication +#endif // ifndef QAPPLICATION_CLASS + +#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 {int} + */ + quint32 instanceId (); + + /** + * @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/linphone-desktop/src/externals/single-application/SingleApplicationPrivate.hpp b/linphone-desktop/src/externals/single-application/SingleApplicationPrivate.hpp new file mode 100644 index 000000000..cc8c8dc50 --- /dev/null +++ b/linphone-desktop/src/externals/single-application/SingleApplicationPrivate.hpp @@ -0,0 +1,82 @@ +// 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 SINGLE_APPLICATION_PRIVATE_H +#define SINGLE_APPLICATION_PRIVATE_H + +#include +#include +#include +#include + +#include "SingleApplication.hpp" + +// ============================================================================= + +struct InstancesInfo { + bool primary; + quint32 secondary; +}; + +class SingleApplicationPrivate : public QObject { + Q_OBJECT + +public: + Q_DECLARE_PUBLIC(SingleApplication) SingleApplicationPrivate (SingleApplication *q_ptr); + ~SingleApplicationPrivate (); + + void genBlockServerName (int msecs); + void startPrimary (bool resetMemory); + void startSecondary (); + void connectToPrimary (int msecs, char connectionType); + + #ifdef Q_OS_UNIX + void crashHandler (); + static void terminate (int signum); + static QList sharedMem; + static QMutex sharedMemMutex; + #endif // ifdef Q_OS_UNIX + + 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 // SINGLE_APPLICATION_PRIVATE_H