diff --git a/linphone-app/CMakeLists.txt b/linphone-app/CMakeLists.txt index 3a95af8e2..454a20d28 100644 --- a/linphone-app/CMakeLists.txt +++ b/linphone-app/CMakeLists.txt @@ -375,7 +375,6 @@ set(HEADERS src/app/proxyModel/SortFilterProxyModel.hpp #src/app/proxyModel/ProxyMapModel.hpp #src/app/proxyModel/ProxyModel.hpp - src/app/single-application/SingleApplication.hpp src/app/translator/DefaultTranslator.hpp src/components/assistant/AssistantModel.hpp src/components/authentication/AuthenticationNotifier.hpp @@ -511,11 +510,16 @@ set(PLUGIN_HEADERS set( UIS) list(APPEND SOURCES include/LinphoneApp/PluginExample.json) +## Single Application +list(APPEND HEADERS src/app/single-application/singleapplication.h + src/app/single-application/singleapplication_p.h) +list(APPEND SOURCES src/app/single-application/singleapplication.cpp + src/app/single-application/singleapplication_p.cpp) set(MAIN_FILE src/app/main.cpp) if (APPLE) list(APPEND SOURCES - src/app/single-application/SingleApplication.cpp + #src/app/single-application/SingleApplication.cpp src/components/core/event-count-notifier/EventCountNotifierMacOs.m src/components/other/desktop-tools/DesktopToolsMacOs.cpp src/components/other/desktop-tools/DesktopToolsMacOsNative.mm @@ -523,31 +527,31 @@ if (APPLE) src/components/other/desktop-tools/state-process/StateProcessMacOs.mm ) list(APPEND HEADERS - src/app/single-application/SingleApplicationPrivate.hpp + #src/app/single-application/SingleApplicationPrivate.hpp src/components/core/event-count-notifier/EventCountNotifierMacOs.hpp src/components/other/desktop-tools/DesktopToolsMacOs.hpp ) elseif (WIN32) list(APPEND SOURCES - src/app/single-application/SingleApplication.cpp + #src/app/single-application/SingleApplication.cpp src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.cpp src/components/other/desktop-tools/DesktopToolsWindows.cpp ) list(APPEND HEADERS - src/app/single-application/SingleApplicationPrivate.hpp + #src/app/single-application/SingleApplicationPrivate.hpp src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.hpp src/components/other/desktop-tools/DesktopToolsWindows.hpp ) else () list(APPEND SOURCES - src/app/single-application/SingleApplicationDBus.cpp + #src/app/single-application/SingleApplicationDBus.cpp src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.cpp src/components/other/desktop-tools/DesktopToolsLinux.cpp src/components/other/desktop-tools/screen-saver/ScreenSaverDBus.cpp src/components/other/desktop-tools/screen-saver/ScreenSaverXdg.cpp ) list(APPEND HEADERS - src/app/single-application/SingleApplicationDBusPrivate.hpp + #src/app/single-application/SingleApplicationDBusPrivate.hpp src/components/core/event-count-notifier/EventCountNotifierSystemTrayIcon.hpp src/components/other/desktop-tools/DesktopToolsLinux.hpp src/components/other/desktop-tools/screen-saver/ScreenSaverDBus.hpp diff --git a/linphone-app/src/app/App.hpp b/linphone-app/src/app/App.hpp index 8ac8958d8..240633dc6 100644 --- a/linphone-app/src/app/App.hpp +++ b/linphone-app/src/app/App.hpp @@ -23,7 +23,9 @@ #include -#include "single-application/SingleApplication.hpp" +#include "single-application/singleapplication.h" + +#include // ============================================================================= diff --git a/linphone-app/src/app/single-application/SingleApplication.cpp b/linphone-app/src/app/single-application/SingleApplication.cpp deleted file mode 100644 index 6ff07eb2c..000000000 --- a/linphone-app/src/app/single-application/SingleApplication.cpp +++ /dev/null @@ -1,421 +0,0 @@ -// 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 -#include -#include - -#ifdef Q_OS_UNIX - #include -#endif // ifdef Q_OS_UNIX - - -#include "utils/Utils.hpp" - -#include "SingleApplication.hpp" -#include "SingleApplicationPrivate.hpp" - -#ifdef Q_OS_WIN - #include - #include -#endif // ifdef Q_OS_WIN -// ============================================================================= - -using namespace std; - -namespace { - constexpr char NewInstance = 'N'; - constexpr char SecondaryInstance = 'S'; - constexpr char Reconnect = 'R'; - constexpr char InvalidConnection = '\0'; -} - -// ----------------------------------------------------------------------------- - -SingleApplicationPrivate::SingleApplicationPrivate (SingleApplication *p_ptr) : q_ptr(p_ptr) { - server = nullptr; - socket = nullptr; -} - -SingleApplicationPrivate::~SingleApplicationPrivate () { - if (socket != nullptr) { - socket->close(); - socket->deleteLater(); - } - memory->lock(); - InstancesInfo *inst = static_cast(memory->data()); - if (server != nullptr) { - server->close(); - server->deleteLater(); - inst->primary = false; - } - memory->unlock(); - memory->deleteLater(); -} - -void SingleApplicationPrivate::genBlockServerName (int timeout) { - QCryptographicHash appData(QCryptographicHash::Sha256); - appData.addData("SingleApplication", 17); - appData.addData(QApplication::applicationName().toUtf8()); - appData.addData(QApplication::organizationName().toUtf8()); - appData.addData(QApplication::organizationDomain().toUtf8()); - - if (!(options & SingleApplication::Mode::ExcludeAppVersion)) { - appData.addData(QApplication::applicationVersion().toUtf8()); - } - - if (!(options & SingleApplication::Mode::ExcludeAppPath)) { - #ifdef Q_OS_WIN - appData.addData(QApplication::applicationFilePath().toLower().toUtf8()); - #else - appData.addData(QApplication::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 - signal(SIGINT, SingleApplicationPrivate::terminate); - #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 = static_cast(memory->data()); - - if (resetMemory) { - inst->primary = true; - inst->secondary = 0; - inst->primaryId = q_ptr->applicationPid(); - } else { - inst->primary = true; - inst->primaryId = q_ptr->applicationPid(); - } - - memory->unlock(); - - instanceNumber = 0; -} - -void SingleApplicationPrivate::startSecondary () { - #ifdef Q_OS_UNIX - signal(SIGINT, SingleApplicationPrivate::terminate); - #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(reinterpret_cast(&instanceNumber), sizeof(quint32)); - initMsg.append(QByteArray::number(qChecksum(initMsg.constData(), uint(initMsg.length())), 256)); - - socket->write(initMsg); - socket->flush(); - socket->waitForBytesWritten(msecs); - } -} - -#ifdef Q_OS_UNIX - void SingleApplicationPrivate::terminate (int signum) { - SingleApplication::instance()->exit(signum); - } -#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(), uint(initMsg.length())), - 256 - ); - tmp = nextConnSocket->read(checksum.length()); - if (checksum == tmp) - break; // Otherwise set to invalid connection (next line) - connectionType = InvalidConnection; - break; - } - default: - connectionType = InvalidConnection; - } - } - } - - if (connectionType == InvalidConnection) { - nextConnSocket->close(); - nextConnSocket->deleteLater(); - return; - } - - QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, [nextConnSocket, instanceId, this]() { - emit this->slotClientConnectionClosed(nextConnSocket, instanceId); - }); - - QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, [nextConnSocket, instanceId, this]() { - emit this->slotDataAvailable(nextConnSocket, instanceId); - }); - - if (connectionType == NewInstance || ( - connectionType == SecondaryInstance && - options & SingleApplication::Mode::SecondaryNotification - ) - ) { - emit q->instanceStarted(); - } - - if (nextConnSocket->bytesAvailable() > 0) { - emit this->slotDataAvailable(nextConnSocket, instanceId); - } -} - -void SingleApplicationPrivate::slotDataAvailable (QLocalSocket *dataSocket, quint32 instanceId) { - Q_Q(SingleApplication); - emit q->receivedMessage(instanceId, dataSocket->readAll()); -} - -void SingleApplicationPrivate::slotClientConnectionClosed (QLocalSocket *closedSocket, quint32 instanceId) { - if (closedSocket->bytesAvailable() > 0) - 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) - : QApplication(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; - } - - // Attempt to attach to the memory segment - if (d->memory->attach()) { - d->memory->lock(); - - InstancesInfo *inst = static_cast(d->memory->data()); - - if (!inst->primary || !Utils::processExists(inst->primaryId)) { // Check if there is not a primary instance and if there is, is it still running? - 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); - d->deleteLater(); - ::exit(EXIT_SUCCESS); -} - -/** - * @brief Destructor - */ -SingleApplication::~SingleApplication () { - Q_D(SingleApplication); - d->deleteLater(); -} - -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; -} - -void SingleApplication::quit () { - QCoreApplication::quit(); -} diff --git a/linphone-app/src/app/single-application/SingleApplication.hpp b/linphone-app/src/app/single-application/SingleApplication.hpp deleted file mode 100644 index c405c5f14..000000000 --- a/linphone-app/src/app/single-application/SingleApplication.hpp +++ /dev/null @@ -1,128 +0,0 @@ -// 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 -#include - -// ============================================================================= - -class SingleApplicationPrivate; - -/** - * @brief The SingleApplication class handles multipe instances of the same - * Application - * @see QCoreApplication - */ -class SingleApplication : public QApplication { - 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 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 - * 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); - virtual ~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); - - virtual void quit (); - -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-app/src/app/single-application/SingleApplicationDBus.cpp b/linphone-app/src/app/single-application/SingleApplicationDBus.cpp deleted file mode 100644 index 675fa8e14..000000000 --- a/linphone-app/src/app/single-application/SingleApplicationDBus.cpp +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-desktop - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include - -#include - -#include "config.h" - -#include "SingleApplication.hpp" -#include "SingleApplicationDBusPrivate.hpp" - -// ============================================================================= - -namespace { - constexpr char ServiceName[] = "org." EXECUTABLE_NAME ".SingleApplication"; -} - -SingleApplicationPrivate::SingleApplicationPrivate (SingleApplication *q_ptr) - : QDBusAbstractAdaptor(q_ptr), q_ptr(q_ptr) {} - -QDBusConnection SingleApplicationPrivate::getBus () const { - if (options & SingleApplication::Mode::User) - return QDBusConnection::sessionBus(); - - return QDBusConnection::systemBus(); -} - -void SingleApplicationPrivate::startPrimary () { - signal(SIGINT, SingleApplicationPrivate::terminate); - if (!getBus().registerObject("/", this, QDBusConnection::ExportAllSlots)) - qWarning() << QStringLiteral("Failed to register single application object on DBus."); - instanceNumber = 0; -} - -void SingleApplicationPrivate::startSecondary () { - signal(SIGINT, SingleApplicationPrivate::terminate); - instanceNumber = 1; -} - -void SingleApplicationPrivate::terminate (int signum) { - SingleApplication::instance()->exit(signum); -} - -SingleApplication::SingleApplication (int &argc, char *argv[], bool allowSecondary, Options options, int) - : QApplication(argc, argv), d_ptr(new SingleApplicationPrivate(this)) { - Q_D(SingleApplication); - - // Store the current mode of the program. - d->options = options; - - if (!d->getBus().isConnected()) { - qWarning() << QStringLiteral("Cannot connect to the D-Bus session bus."); - delete d; - ::exit(EXIT_FAILURE); - } - - if (d->getBus().registerService(ServiceName)) { - d->startPrimary(); - return; - } - - if (allowSecondary) { - d->startSecondary(); - return; - } - - delete d; - ::exit(EXIT_SUCCESS); -} - -SingleApplication::~SingleApplication () { - Q_D(SingleApplication); - delete d; -} - -bool SingleApplication::isPrimary () { - Q_D(SingleApplication); - return d->instanceNumber == 0; -} - -bool SingleApplication::isSecondary () { - Q_D(SingleApplication); - return d->instanceNumber != 0; -} - -quint32 SingleApplication::instanceId () { - Q_D(SingleApplication); - return d->instanceNumber; -} - -bool SingleApplication::sendMessage (QByteArray message, int timeout) { - Q_D(SingleApplication); - - if (isPrimary()) return false; - - QDBusInterface iface(ServiceName, "/", "", d->getBus()); - if (iface.isValid()) { - iface.setTimeout(timeout); - iface.call(QDBus::Block, "handleMessageReceived", instanceId(), message); - return true; - } - - return false; -} - -void SingleApplicationPrivate::handleMessageReceived (quint32 instanceId, QByteArray message) { - Q_Q(SingleApplication); - emit q->receivedMessage(instanceId, message); -} - -void SingleApplication::quit () { - QCoreApplication::quit(); -} diff --git a/linphone-app/src/app/single-application/SingleApplicationDBusPrivate.hpp b/linphone-app/src/app/single-application/SingleApplicationDBusPrivate.hpp deleted file mode 100644 index b98167aef..000000000 --- a/linphone-app/src/app/single-application/SingleApplicationDBusPrivate.hpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-desktop - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#ifndef SINGLE_APPLICATION_DBUS_PRIVATE_H_ -#define SINGLE_APPLICATION_DBUS_PRIVATE_H_ - -#include -#include - -#include "SingleApplication.hpp" - -// ============================================================================= - -struct InstancesInfo { - bool primary; - quint32 secondary; -}; - -class SingleApplicationPrivate : public QDBusAbstractAdaptor { - Q_OBJECT; - Q_CLASSINFO("D-Bus Interface", "org.linphone.DBus.SingleApplication"); - -public: - SingleApplicationPrivate (SingleApplication *q_ptr); - - QDBusConnection getBus () const; - - void startPrimary (); - void startSecondary (); - - static void terminate (int signum); - - SingleApplication *q_ptr; - SingleApplication::Options options; - quint32 instanceNumber; - -// Explicit public slot. Cannot be private, must be exported as a method via D-Bus. -public slots: - void handleMessageReceived (quint32 instanceId, QByteArray message); - -private: - Q_DECLARE_PUBLIC(SingleApplication); -}; - -#endif // SINGLE_APPLICATION_DBUS_PRIVATE_H_ diff --git a/linphone-app/src/app/single-application/SingleApplicationPrivate.hpp b/linphone-app/src/app/single-application/SingleApplicationPrivate.hpp deleted file mode 100644 index 8db9a288e..000000000 --- a/linphone-app/src/app/single-application/SingleApplicationPrivate.hpp +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-desktop - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -// 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; - qint64 primaryId; -}; - -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 - static void terminate (int signum); - #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_ diff --git a/linphone-app/src/app/single-application/singleapplication.cpp b/linphone-app/src/app/single-application/singleapplication.cpp new file mode 100644 index 000000000..3e8fcb5ed --- /dev/null +++ b/linphone-app/src/app/single-application/singleapplication.cpp @@ -0,0 +1,273 @@ +// Copyright (c) Itay Grudev 2015 - 2023 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// Permission is not granted to use this software or any of the associated files +// as sample data for the purposes of building machine learning models. +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include +#include +#include + +#include "singleapplication.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 + */ +SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, const QString &userData ) + : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) +{ + Q_D( SingleApplication ); + +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + // On Android and iOS since the library is not supported fallback to + // standard QApplication behaviour by simply returning at this point. + qWarning() << "SingleApplication is not supported on Android and iOS systems."; + return; +#endif + + // Store the current mode of the program + d->options = options; + + // Add any unique user data + if ( ! userData.isEmpty() ) + d->addAppData( userData ); + + // 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 + SingleApplicationPrivate::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. + d->memory = new QSharedMemory( d->blockServerName ); + d->memory->attach(); + delete d->memory; +#endif + // Guarantee thread safe behaviour with a shared memory block. + d->memory = new QSharedMemory( d->blockServerName ); + + // Create a shared memory block + if( d->memory->create( sizeof( InstancesInfo ) )){ + // Initialize the shared memory block + if( ! d->memory->lock() ){ + qCritical() << "SingleApplication: Unable to lock memory block after create."; + abortSafely(); + } + d->initializeMemoryBlock(); + } else { + if( d->memory->error() == QSharedMemory::AlreadyExists ){ + // Attempt to attach to the memory segment + if( ! d->memory->attach() ){ + qCritical() << "SingleApplication: Unable to attach to shared memory block."; + abortSafely(); + } + if( ! d->memory->lock() ){ + qCritical() << "SingleApplication: Unable to lock memory block after attach."; + abortSafely(); + } + } else { + qCritical() << "SingleApplication: Unable to create block."; + abortSafely(); + } + } + + auto *inst = static_cast( d->memory->data() ); + QElapsedTimer time; + time.start(); + + // Make sure the shared memory block is initialised and in consistent state + while( true ){ + // If the shared memory block's checksum is valid continue + if( d->blockChecksum() == inst->checksum ) break; + + // If more than 5s have elapsed, assume the primary instance crashed and + // assume it's position + if( time.elapsed() > 5000 ){ + qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. 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 initialise faster + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory for random wait."; + qDebug() << d->memory->errorString(); + } + SingleApplicationPrivate::randomSleep(); + if( ! d->memory->lock() ){ + qCritical() << "SingleApplication: Unable to lock memory after random wait."; + abortSafely(); + } + } + + if( inst->primary == false ){ + d->startPrimary(); + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory after primary start."; + qDebug() << d->memory->errorString(); + } + return; + } + + // Check if another instance can be started + if( allowSecondary ){ + d->startSecondary(); + if( d->options & Mode::SecondaryNotification ){ + d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance ); + } + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory after secondary start."; + qDebug() << d->memory->errorString(); + } + return; + } + + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory at end of execution."; + qDebug() << d->memory->errorString(); + } + + d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); + + delete d; + + ::exit( EXIT_SUCCESS ); +} + +SingleApplication::~SingleApplication() +{ + Q_D( SingleApplication ); + delete d; +} + +/** + * Checks if the current application instance is primary. + * @return Returns true if the instance is primary, false otherwise. + */ +bool SingleApplication::isPrimary() const +{ + Q_D( const SingleApplication ); + return d->server != nullptr; +} + +/** + * Checks if the current application instance is secondary. + * @return Returns true if the instance is secondary, false otherwise. + */ +bool SingleApplication::isSecondary() const +{ + Q_D( const SingleApplication ); + 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 SingleApplication::instanceId() const +{ + Q_D( const SingleApplication ); + 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 SingleApplication::primaryPid() const +{ + Q_D( const SingleApplication ); + return d->primaryPid(); +} + +/** + * Returns the username the primary instance is running as. + * @return Returns the username the primary instance is running as. + */ +QString SingleApplication::primaryUser() const +{ + Q_D( const SingleApplication ); + return d->primaryUser(); +} + +/** + * Returns the username the current instance is running as. + * @return Returns the username the current instance is running as. + */ +QString SingleApplication::currentUser() const +{ + return SingleApplicationPrivate::getUsername(); +} + +/** + * Sends message to the Primary Instance. + * @param message The message to send. + * @param timeout the maximum timeout in milliseconds for blocking functions. + * @param sendMode mode of operation + * @return true if the message was sent successfuly, false otherwise. + */ +bool SingleApplication::sendMessage( const QByteArray &message, int timeout, SendMode sendMode ) +{ + Q_D( SingleApplication ); + + // Nobody to connect to + if( isPrimary() ) return false; + + // Make sure the socket is connected + if( ! d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect ) ) + return false; + + return d->writeConfirmedMessage( timeout, message, sendMode ); +} + +/** + * Cleans up the shared memory block and exits with a failure. + * This function halts program execution. + */ +void SingleApplication::abortSafely() +{ + Q_D( SingleApplication ); + + qCritical() << "SingleApplication: " << d->memory->error() << d->memory->errorString(); + delete d; + ::exit( EXIT_FAILURE ); +} + +QStringList SingleApplication::userData() const +{ + Q_D( const SingleApplication ); + return d->appData(); +} diff --git a/linphone-app/src/app/single-application/singleapplication.h b/linphone-app/src/app/single-application/singleapplication.h new file mode 100644 index 000000000..0f241580e --- /dev/null +++ b/linphone-app/src/app/single-application/singleapplication.h @@ -0,0 +1,187 @@ +// Copyright (c) Itay Grudev 2015 - 2023 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// Permission is not granted to use this software or any of the associated files +// as sample data for the purposes of building machine learning models. +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// V3.4.0 +#ifndef SINGLE_APPLICATION_H +#define SINGLE_APPLICATION_H + +#include +#include + +#ifndef QAPPLICATION_CLASS + #define QAPPLICATION_CLASS QApplication +#endif + +#include QT_STRINGIFY(QAPPLICATION_CLASS) + +class SingleApplicationPrivate; + +/** + * @brief Handles multiple instances of the same + * Application + * @see QCoreApplication + */ +class SingleApplication : public QAPPLICATION_CLASS +{ + Q_OBJECT + + using app_t = QAPPLICATION_CLASS; + +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 Mode { + /** The `SingleApplication` block should apply user wide + * (this adds user specific data to the key used for the shared memory and server name) + * */ + User = 1 << 0, + /** + * The `SingleApplication` block applies system-wide. + */ + System = 1 << 1, + /** + * Whether to trigger `instanceStarted()` even whenever secondary instances are started + */ + SecondaryNotification = 1 << 2, + /** + * Excludes the application version from the server name (and memory block) hash + */ + ExcludeAppVersion = 1 << 3, + /** + * Excludes the application path from the server name (and memory block) hash + */ + ExcludeAppPath = 1 << 4 + }; + Q_DECLARE_FLAGS(Options, Mode) + + /** + * @brief Intitializes a `SingleApplication` instance with argc command line + * arguments in argv + * @arg argc - Number of arguments in argv + * @arg argv - Supplied command line arguments + * @arg allowSecondary - Whether to start the instance as secondary + * if there is already a primary instance. + * @arg mode - Whether for the `SingleApplication` block to be applied + * User wide or System wide. + * @arg 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` + * 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 = 1000, const QString &userData = {} ); + ~SingleApplication() override; + + /** + * @brief Checks if the instance is primary instance + * @returns `true` if the instance is primary + */ + bool isPrimary() const; + + /** + * @brief Checks if the instance is a secondary instance + * @returns `true` if the instance is secondary + */ + bool isSecondary() const; + + /** + * @brief Returns a unique identifier for the current instance + * @returns instance id + */ + quint32 instanceId() const; + + /** + * @brief Returns the process ID (PID) of the primary instance + * @returns pid + */ + qint64 primaryPid() const; + + /** + * @brief Returns the username of the user running the primary instance + * @returns user name + */ + QString primaryUser() const; + + /** + * @brief Returns the username of the current user + * @returns user name + */ + QString currentUser() const; + + /** + * @brief Mode of operation of sendMessage. + */ + enum SendMode { + NonBlocking, /** Do not wait for the primary instance termination and return immediately */ + BlockUntilPrimaryExit, /** Wait until the primary instance is terminated */ + }; + + /** + * @brief Sends a message to the primary instance + * @param message data to send + * @param timeout timeout for connecting + * @param sendMode - Mode of operation + * @returns `true` on success + * @note sendMessage() will return false if invoked from the primary instance + */ + bool sendMessage( const QByteArray &message, int timeout = 100, SendMode sendMode = NonBlocking ); + + /** + * @brief Get the set user data. + * @returns user data + */ + QStringList userData() const; + +Q_SIGNALS: + /** + * @brief Triggered whenever a new instance had been started, + * except for secondary instances if the `Mode::SecondaryNotification` flag is not specified + */ + void instanceStarted(); + + /** + * @brief Triggered whenever there is a message received from a secondary instance + */ + void receivedMessage( quint32 instanceId, QByteArray message ); + +private: + SingleApplicationPrivate *d_ptr; + Q_DECLARE_PRIVATE(SingleApplication) + void abortSafely(); +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options) + +#endif // SINGLE_APPLICATION_H diff --git a/linphone-app/src/app/single-application/singleapplication_p.cpp b/linphone-app/src/app/single-application/singleapplication_p.cpp new file mode 100644 index 000000000..b13ba5812 --- /dev/null +++ b/linphone-app/src/app/single-application/singleapplication_p.cpp @@ -0,0 +1,551 @@ +// Copyright (c) Itay Grudev 2015 - 2023 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// Permission is not granted to use this software or any of the associated files +// as sample data for the purposes of building machine learning models. +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// +// 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. +// + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) +#include +#else +#include +#endif + +#include "singleapplication.h" +#include "singleapplication_p.h" + +#ifdef Q_OS_UNIX + #include + #include + #include +#endif + +#ifdef Q_OS_WIN + #ifndef NOMINMAX + #define NOMINMAX 1 + #endif + #include + #include +#endif + +SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr ) + : q_ptr( q_ptr ) +{ + server = nullptr; + socket = nullptr; + memory = nullptr; + instanceNumber = 0; +} + +SingleApplicationPrivate::~SingleApplicationPrivate() +{ + if( socket != nullptr ){ + socket->close(); + socket->deleteLater(); + } + + if( memory != nullptr ){ + memory->lock(); + auto *inst = static_cast(memory->data()); + if( server != nullptr ){ + server->close(); + server->deleteLater(); + inst->primary = false; + inst->primaryPid = -1; + inst->primaryUser[0] = '\0'; + inst->checksum = blockChecksum(); + } + memory->unlock(); + + memory->deleteLater(); + } +} + +QString SingleApplicationPrivate::getUsername() +{ +#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 ); +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + return QString::fromLocal8Bit( qgetenv( "USERNAME" ) ); +#else + return qEnvironmentVariable( "USERNAME" ); +#endif +#endif +#ifdef Q_OS_UNIX + QString username; + uid_t uid = geteuid(); + struct passwd *pw = getpwuid( uid ); + if( pw ) + username = QString::fromLocal8Bit( pw->pw_name ); + if ( username.isEmpty() ){ +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + username = QString::fromLocal8Bit( qgetenv( "USER" ) ); +#else + username = qEnvironmentVariable( "USER" ); +#endif + } + return username; +#endif +} + +void SingleApplicationPrivate::genBlockServerName() +{ + QCryptographicHash appData( QCryptographicHash::Sha256 ); +#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0) + appData.addData( "SingleApplication", 17 ); +#else + appData.addData( QByteArrayView{"SingleApplication"} ); +#endif + appData.addData( SingleApplication::app_t::applicationName().toUtf8() ); + appData.addData( SingleApplication::app_t::organizationName().toUtf8() ); + appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() ); + + if ( ! appDataList.isEmpty() ) + appData.addData( appDataList.join(QString()).toUtf8() ); + + if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ){ + appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() ); + } + + if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){ +#if defined(Q_OS_WIN) + appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); +#elif defined(Q_OS_LINUX) + // If the application is running as an AppImage then the APPIMAGE env var should be used + // instead of applicationPath() as each instance is launched with its own executable path + const QByteArray appImagePath = qgetenv( "APPIMAGE" ); + if( appImagePath.isEmpty() ){ // Not running as AppImage: use path to executable file + appData.addData( SingleApplication::app_t::applicationFilePath().toUtf8() ); + } else { // Running as AppImage: Use absolute path to AppImage file + appData.addData( appImagePath ); + }; +#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 ){ + appData.addData( getUsername().toUtf8() ); + } + + // Replace the backslash in RFC 2045 Base64 [a-zA-Z0-9+/=] to comply with + // server naming requirements. + blockServerName = QString::fromUtf8(appData.result().toBase64().replace("/", "_")); +} + +void SingleApplicationPrivate::initializeMemoryBlock() const +{ + auto *inst = static_cast( memory->data() ); + inst->primary = false; + inst->secondary = 0; + inst->primaryPid = -1; + inst->primaryUser[0] = '\0'; + inst->checksum = blockChecksum(); +} + +void SingleApplicationPrivate::startPrimary() +{ + // Reset the number of connections + auto *inst = static_cast ( memory->data() ); + + inst->primary = true; + inst->primaryPid = QCoreApplication::applicationPid(); + qstrncpy( inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser) ); + inst->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(); + + // 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 + ); +} + +void SingleApplicationPrivate::startSecondary() +{ + auto *inst = static_cast ( memory->data() ); + + inst->secondary += 1; + inst->checksum = blockChecksum(); + instanceNumber = inst->secondary; +} + +bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType ) +{ + QElapsedTimer time; + time.start(); + + // Connect to the Local Server of the Primary Instance if not already + // connected. + if( socket == nullptr ){ + socket = new QLocalSocket(); + } + + if( socket->state() == QLocalSocket::ConnectedState ) return true; + + if( socket->state() != QLocalSocket::ConnectedState ){ + + while( true ){ + randomSleep(); + + if( socket->state() != QLocalSocket::ConnectingState ) + socket->connectToServer( blockServerName ); + + if( socket->state() == QLocalSocket::ConnectingState ){ + socket->waitForConnected( static_cast(msecs - 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() >= msecs ) return false; + } + } + + // Initialisation message according to the SingleApplication protocol + QByteArray initMsg; + QDataStream writeStream(&initMsg, QIODevice::WriteOnly); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + writeStream.setVersion(QDataStream::Qt_5_6); +#endif + + writeStream << blockServerName.toLatin1(); + writeStream << static_cast(connectionType); + writeStream << instanceNumber; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + quint16 checksum = qChecksum(QByteArray(initMsg.constData(), static_cast(initMsg.length()))); +#else + quint16 checksum = qChecksum(initMsg.constData(), static_cast(initMsg.length())); +#endif + writeStream << checksum; + + return writeConfirmedMessage( static_cast(msecs - time.elapsed()), initMsg ); +} + +void SingleApplicationPrivate::writeAck( QLocalSocket *sock ) { + sock->putChar('\n'); +} + +bool SingleApplicationPrivate::writeConfirmedMessage (int msecs, const QByteArray &msg, SingleApplication::SendMode sendMode) +{ + QElapsedTimer time; + time.start(); + + // Frame 1: The header indicates the message length that follows + QByteArray header; + QDataStream headerStream(&header, QIODevice::WriteOnly); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + headerStream.setVersion(QDataStream::Qt_5_6); +#endif + headerStream << static_cast ( msg.length() ); + + if( ! writeConfirmedFrame( static_cast(msecs - time.elapsed()), header )) + return false; + + // Frame 2: The message + const bool result = writeConfirmedFrame( static_cast(msecs - time.elapsed()), msg ); + + // Block if needed + if (socket && sendMode == SingleApplication::BlockUntilPrimaryExit) + socket->waitForDisconnected(-1); + + return result; +} + +bool SingleApplicationPrivate::writeConfirmedFrame( int msecs, const QByteArray &msg ) +{ + socket->write( msg ); + socket->flush(); + + bool result = socket->waitForReadyRead( msecs ); // await ack byte + if (result) { + socket->read( 1 ); + return true; + } + + return false; +} + +quint16 SingleApplicationPrivate::blockChecksum() const +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + quint16 checksum = qChecksum(QByteArray(static_cast(memory->constData()), offsetof(InstancesInfo, checksum))); +#else + quint16 checksum = qChecksum(static_cast(memory->constData()), offsetof(InstancesInfo, checksum)); +#endif + return checksum; +} + +qint64 SingleApplicationPrivate::primaryPid() const +{ + qint64 pid; + + memory->lock(); + auto *inst = static_cast( memory->data() ); + pid = inst->primaryPid; + memory->unlock(); + + return pid; +} + +QString SingleApplicationPrivate::primaryUser() const +{ + QByteArray username; + + memory->lock(); + auto *inst = static_cast( memory->data() ); + username = inst->primaryUser; + memory->unlock(); + + return QString::fromUtf8( username ); +} + +/** + * @brief Executed when a connection has been made to the LocalServer + */ +void SingleApplicationPrivate::slotConnectionEstablished() +{ + QLocalSocket *nextConnSocket = server->nextPendingConnection(); + connectionMap.insert(nextConnSocket, ConnectionInfo()); + + QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this, + [nextConnSocket, this](){ + auto &info = connectionMap[nextConnSocket]; + this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); + } + ); + + QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater); + + QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this, + [nextConnSocket, this](){ + connectionMap.remove(nextConnSocket); + } + ); + + QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this, + [nextConnSocket, this](){ + auto &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: + this->slotDataAvailable( nextConnSocket, info.instanceId ); + break; + default: + break; + }; + } + ); +} + +void SingleApplicationPrivate::readMessageHeader( QLocalSocket *sock, SingleApplicationPrivate::ConnectionStage nextStage ) +{ + if (!connectionMap.contains( sock )){ + return; + } + + if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){ + return; + } + + QDataStream headerStream( sock ); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + headerStream.setVersion( QDataStream::Qt_5_6 ); +#endif + + // 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 SingleApplicationPrivate::isFrameComplete( QLocalSocket *sock ) +{ + if (!connectionMap.contains( sock )){ + return false; + } + + ConnectionInfo &info = connectionMap[sock]; + if( sock->bytesAvailable() < ( qint64 )info.msgLen ){ + return false; + } + + return true; +} + +void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) +{ + Q_Q(SingleApplication); + + if( !isFrameComplete( sock ) ) + return; + + // Read the message body + QByteArray msgBytes = sock->readAll(); + QDataStream readStream(msgBytes); + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + readStream.setVersion( QDataStream::Qt_5_6 ); +#endif + + // server name + QByteArray latin1Name; + readStream >> latin1Name; + + // connection type + ConnectionType connectionType = InvalidConnection; + quint8 connTypeVal = InvalidConnection; + readStream >> connTypeVal; + connectionType = static_cast ( connTypeVal ); + + // instance id + quint32 instanceId = 0; + readStream >> instanceId; + + // checksum + quint16 msgChecksum = 0; + readStream >> msgChecksum; + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const quint16 actualChecksum = qChecksum(QByteArray(msgBytes.constData(), static_cast(msgBytes.length() - sizeof(quint16)))); +#else + const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast(msgBytes.length() - sizeof(quint16))); +#endif + + bool isValid = readStream.status() == QDataStream::Ok && + QLatin1String(latin1Name) == blockServerName && + msgChecksum == actualChecksum; + + if( !isValid ){ + sock->close(); + return; + } + + ConnectionInfo &info = connectionMap[sock]; + info.instanceId = instanceId; + info.stage = StageConnectedHeader; + + if( connectionType == NewInstance || + ( connectionType == SecondaryInstance && + options & SingleApplication::Mode::SecondaryNotification ) ) + { + Q_EMIT q->instanceStarted(); + } + + writeAck( sock ); +} + +void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId ) +{ + Q_Q(SingleApplication); + + if ( !isFrameComplete( dataSocket ) ) + return; + + const QByteArray message = dataSocket->readAll(); + + writeAck( dataSocket ); + + ConnectionInfo &info = connectionMap[dataSocket]; + info.stage = StageConnectedHeader; + + Q_EMIT q->receivedMessage( instanceId, message); +} + +void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId ) +{ + if( closedSocket->bytesAvailable() > 0 ) + slotDataAvailable( closedSocket, instanceId ); +} + +void SingleApplicationPrivate::randomSleep() +{ +#if QT_VERSION >= QT_VERSION_CHECK( 5, 10, 0 ) + QThread::msleep( QRandomGenerator::global()->bounded( 8u, 18u )); +#else + qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits::max() ); + QThread::msleep( qrand() % 11 + 8); +#endif +} + +void SingleApplicationPrivate::addAppData(const QString &data) +{ + appDataList.push_back(data); +} + +QStringList SingleApplicationPrivate::appData() const +{ + return appDataList; +} diff --git a/linphone-app/src/app/single-application/singleapplication_p.h b/linphone-app/src/app/single-application/singleapplication_p.h new file mode 100644 index 000000000..6b21516f5 --- /dev/null +++ b/linphone-app/src/app/single-application/singleapplication_p.h @@ -0,0 +1,110 @@ +// Copyright (c) Itay Grudev 2015 - 2023 +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// Permission is not granted to use this software or any of the associated files +// as sample data for the purposes of building machine learning models. +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// +// 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 +#include +#include +#include "singleapplication.h" + +struct InstancesInfo { + bool primary; + quint32 secondary; + qint64 primaryPid; + char primaryUser[128]; + quint16 checksum; // Must be the last field +}; + +struct ConnectionInfo { + qint64 msgLen = 0; + quint32 instanceId = 0; + quint8 stage = 0; +}; + +class SingleApplicationPrivate : public QObject { +Q_OBJECT +public: + 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(SingleApplication) + + SingleApplicationPrivate( SingleApplication *q_ptr ); + ~SingleApplicationPrivate() override; + + static QString getUsername(); + void genBlockServerName(); + void initializeMemoryBlock() const; + void startPrimary(); + void startSecondary(); + bool connectToPrimary( int msecs, ConnectionType connectionType ); + quint16 blockChecksum() const; + qint64 primaryPid() const; + QString primaryUser() const; + bool isFrameComplete(QLocalSocket *sock); + void readMessageHeader(QLocalSocket *socket, ConnectionStage nextStage); + void readInitMessageBody(QLocalSocket *socket); + void writeAck(QLocalSocket *sock); + bool writeConfirmedFrame(int msecs, const QByteArray &msg); + bool writeConfirmedMessage(int msecs, const QByteArray &msg, SingleApplication::SendMode sendMode = SingleApplication::NonBlocking); + static void randomSleep(); + void addAppData(const QString &data); + QStringList appData() const; + + SingleApplication *q_ptr; + QSharedMemory *memory; + QLocalSocket *socket; + QLocalServer *server; + quint32 instanceNumber; + QString blockServerName; + SingleApplication::Options options; + QMap connectionMap; + QStringList appDataList; + +public Q_SLOTS: + void slotConnectionEstablished(); + void slotDataAvailable( QLocalSocket*, quint32 ); + void slotClientConnectionClosed( QLocalSocket*, quint32 ); +}; + +#endif // SINGLEAPPLICATION_P_H