From b6ff6253707db774126cc0ad9831c037528b69b0 Mon Sep 17 00:00:00 2001 From: Julien Wadel Date: Mon, 9 Oct 2023 17:17:34 +0200 Subject: [PATCH] Add Single Application. Add command parser and output modes (--verbose --qt-logs-only). Move static Logger to core as it is used from Qt and dispatch logs to the SDK thread. Update SDK to allow builds from MSVC 2022. --- Linphone/CMakeLists.txt | 4 +- Linphone/core/App.cpp | 44 +- Linphone/core/App.hpp | 13 +- Linphone/core/CMakeLists.txt | 13 +- .../logger/QtLogger.cpp} | 20 +- .../logger/QtLogger.hpp} | 11 +- Linphone/core/singleapplication/README.md | 10 + .../SingleApplicationDBus.cpp | 128 +++++ .../SingleApplicationDBusPrivate.hpp | 62 +++ .../singleapplication/singleapplication.cpp | 262 +++++++++ .../singleapplication/singleapplication.h | 192 +++++++ .../singleapplication/singleapplication_p.cpp | 510 ++++++++++++++++++ .../singleapplication/singleapplication_p.h | 107 ++++ Linphone/main.cpp | 4 +- Linphone/model/CMakeLists.txt | 1 - Linphone/model/logger/LoggerListener.cpp | 2 +- Linphone/model/logger/LoggerListener.hpp | 6 +- Linphone/model/logger/LoggerModel.cpp | 55 +- Linphone/model/logger/LoggerModel.hpp | 8 +- external/linphone-sdk | 2 +- 20 files changed, 1400 insertions(+), 54 deletions(-) rename Linphone/{model/logger/LoggerStaticModel.cpp => core/logger/QtLogger.cpp} (68%) rename Linphone/{model/logger/LoggerStaticModel.hpp => core/logger/QtLogger.hpp} (82%) create mode 100644 Linphone/core/singleapplication/README.md create mode 100644 Linphone/core/singleapplication/SingleApplicationDBus.cpp create mode 100644 Linphone/core/singleapplication/SingleApplicationDBusPrivate.hpp create mode 100644 Linphone/core/singleapplication/singleapplication.cpp create mode 100644 Linphone/core/singleapplication/singleapplication.h create mode 100644 Linphone/core/singleapplication/singleapplication_p.cpp create mode 100644 Linphone/core/singleapplication/singleapplication_p.h diff --git a/Linphone/CMakeLists.txt b/Linphone/CMakeLists.txt index f015f0e37..7b13acb7b 100644 --- a/Linphone/CMakeLists.txt +++ b/Linphone/CMakeLists.txt @@ -34,10 +34,12 @@ set(APP_TARGETS ${LinphoneCxx_TARGET}) set(QT_DEFAULT_MAJOR_VERSION 6) set(QT_PACKAGES Core Quick Qml Widgets)# Search Core at first for initialize Qt scripts for next find_packages. +if (UNIX AND NOT APPLE) + list(APPEND QT_PACKAGES DBus) +endif() find_package(Qt6 REQUIRED COMPONENTS Core) find_package(Qt6 REQUIRED COMPONENTS ${QT_PACKAGES}) - if(${Qt6_VERSION} VERSION_LESS "6.3.0") set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) diff --git a/Linphone/core/App.cpp b/Linphone/core/App.cpp index 4d5e22065..075ef9bb5 100644 --- a/Linphone/core/App.cpp +++ b/Linphone/core/App.cpp @@ -22,13 +22,16 @@ #include +#include "core/logger/QtLogger.hpp" +#include "core/singleapplication/singleapplication.h" #include "tool/Constants.hpp" #include "view/Page/LoginPage.hpp" -App::App(QObject *parent) : QObject(parent) { +App::App(int &argc, char *argv[]) + : SingleApplication(argc, argv, true, Mode::User | Mode::ExcludeAppPath | Mode::ExcludeAppVersion) { mLinphoneThread = new Thread(this); init(); - qDebug() << "Starting Thread"; + qDebug() << "[App] Starting Thread"; mLinphoneThread->start(); } @@ -37,9 +40,20 @@ App::App(QObject *parent) : QObject(parent) { //----------------------------------------------------------- void App::init() { - // Core + // Core. Manage the logger so it must be instantiate at first. mCoreModel = QSharedPointer::create("", mLinphoneThread); + // Console Commands + createCommandParser(); + mParser->parse(this->arguments()); + // TODO : Update languages for command translations. + + createCommandParser(); // Recreate parser in order to use translations from config. + mParser->process(*this); + + if (mParser->isSet("verbose")) QtLogger::enableVerbose(true); + if (mParser->isSet("qt-logs-only")) QtLogger::enableQtOnly(true); + connect(mLinphoneThread, &QThread::started, mCoreModel.get(), &CoreModel::start); // QML mEngine = new QQmlApplicationEngine(this); @@ -62,3 +76,27 @@ void App::initCppInterfaces() { Constants::MainQmlUri, 1, 0, "LoginPageCpp", [](QQmlEngine *engine, QJSEngine *) -> QObject * { return new LoginPage(engine); }); } + +//------------------------------------------------------------ +void App::createCommandParser() { + if (!mParser) delete mParser; + + mParser = new QCommandLineParser(); + mParser->setApplicationDescription(tr("applicationDescription")); + mParser->addPositionalArgument("command", tr("commandLineDescription").replace("%1", APPLICATION_NAME), + "[command]"); + mParser->addOptions({ + {{"h", "help"}, tr("commandLineOptionHelp")}, + {"cli-help", tr("commandLineOptionCliHelp").replace("%1", APPLICATION_NAME)}, + {{"v", "version"}, tr("commandLineOptionVersion")}, + {"config", tr("commandLineOptionConfig").replace("%1", EXECUTABLE_NAME), tr("commandLineOptionConfigArg")}, + {"fetch-config", tr("commandLineOptionFetchConfig").replace("%1", EXECUTABLE_NAME), + tr("commandLineOptionFetchConfigArg")}, + {{"c", "call"}, tr("commandLineOptionCall").replace("%1", EXECUTABLE_NAME), tr("commandLineOptionCallArg")}, +#ifndef Q_OS_MACOS + {"iconified", tr("commandLineOptionIconified")}, +#endif // ifndef Q_OS_MACOS + {{"V", "verbose"}, tr("commandLineOptionVerbose")}, + {"qt-logs-only", tr("commandLineOptionQtLogsOnly")}, + }); +} diff --git a/Linphone/core/App.hpp b/Linphone/core/App.hpp index 8310910a3..c8f3fdaef 100644 --- a/Linphone/core/App.hpp +++ b/Linphone/core/App.hpp @@ -19,14 +19,16 @@ */ #include +#include #include #include "core/thread/Thread.hpp" +#include "core/singleapplication/singleapplication.h" #include "model/core/CoreModel.hpp" -class App : public QObject { +class App : public SingleApplication { public: - App(QObject *parent = nullptr); + App(int &argc, char *argv[]); void init(); void initCppInterfaces(); @@ -36,4 +38,9 @@ public: QQmlApplicationEngine *mEngine = nullptr; Thread *mLinphoneThread = nullptr; QSharedPointer mCoreModel; -}; \ No newline at end of file + +private: + void createCommandParser(); + + QCommandLineParser *mParser = nullptr; +}; diff --git a/Linphone/core/CMakeLists.txt b/Linphone/core/CMakeLists.txt index 93cd07320..4861aa859 100644 --- a/Linphone/core/CMakeLists.txt +++ b/Linphone/core/CMakeLists.txt @@ -1,9 +1,20 @@ list(APPEND _LINPHONEAPP_SOURCES core/App.cpp + core/logger/QtLogger.cpp core/path/Paths.cpp core/setting/Settings.cpp core/thread/Thread.cpp - ) +## Single Application +if(APPLE OR WIN32) + list(APPEND _LINPHONEAPP_SOURCES core/singleapplication/singleapplication.cpp + core/singleapplication/singleapplication_p.cpp) +else() # Use QDBus for Linux + list(APPEND _LINPHONEAPP_SOURCES + core/singleapplication/singleapplication.h #Added for Moc + core/singleapplication/SingleApplicationDBusPrivate.hpp + core/singleapplication/SingleApplicationDBus.cpp) +endif() + set(_LINPHONEAPP_SOURCES ${_LINPHONEAPP_SOURCES} PARENT_SCOPE) diff --git a/Linphone/model/logger/LoggerStaticModel.cpp b/Linphone/core/logger/QtLogger.cpp similarity index 68% rename from Linphone/model/logger/LoggerStaticModel.cpp rename to Linphone/core/logger/QtLogger.cpp index c11157f9f..7c3ed9c05 100644 --- a/Linphone/model/logger/LoggerStaticModel.cpp +++ b/Linphone/core/logger/QtLogger.cpp @@ -18,20 +18,28 @@ * along with this program. If not, see . */ -#include "LoggerStaticModel.hpp" +#include "QtLogger.hpp" #include // ----------------------------------------------------------------------------- -static LoggerStaticModel gLogger; +static QtLogger gLogger; -LoggerStaticModel::LoggerStaticModel(QObject *parent) : QObject(parent) { - qInstallMessageHandler(LoggerStaticModel::onQtLog); +QtLogger::QtLogger(QObject *parent) : QObject(parent) { + qInstallMessageHandler(QtLogger::onQtLog); } -LoggerStaticModel *LoggerStaticModel::getInstance() { +QtLogger *QtLogger::getInstance() { return &gLogger; } -void LoggerStaticModel::onQtLog(QtMsgType type, const QMessageLogContext &context, const QString &msg) { +void QtLogger::onQtLog(QtMsgType type, const QMessageLogContext &context, const QString &msg) { emit gLogger.logReceived(type, context.file, context.line, msg); } + +void QtLogger::enableVerbose(bool verbose) { + emit gLogger.requestVerboseEnabled(verbose); +} + +void QtLogger::enableQtOnly(bool qtOnly) { + emit gLogger.requestQtOnlyEnabled(qtOnly); +} diff --git a/Linphone/model/logger/LoggerStaticModel.hpp b/Linphone/core/logger/QtLogger.hpp similarity index 82% rename from Linphone/model/logger/LoggerStaticModel.hpp rename to Linphone/core/logger/QtLogger.hpp index 9e0c17b83..0d7afe7c7 100644 --- a/Linphone/model/logger/LoggerStaticModel.hpp +++ b/Linphone/core/logger/QtLogger.hpp @@ -27,16 +27,21 @@ // ============================================================================= // Only one instance. Use getInstance() and logReceived() to bind logs coming from Qt. -class LoggerStaticModel : public QObject { +class QtLogger : public QObject { Q_OBJECT public: - LoggerStaticModel(QObject *parent = nullptr); + QtLogger(QObject *parent = nullptr); - static LoggerStaticModel *getInstance(); + static QtLogger *getInstance(); static void onQtLog(QtMsgType type, const QMessageLogContext &context, const QString &msg); + static void enableVerbose(bool verbose); + static void enableQtOnly(bool qtOnly); + signals: void logReceived(QtMsgType type, QString contextFile, int contextLine, QString msg); + void requestVerboseEnabled(bool verbose); + void requestQtOnlyEnabled(bool qtOnly); }; #endif diff --git a/Linphone/core/singleapplication/README.md b/Linphone/core/singleapplication/README.md new file mode 100644 index 000000000..aa75fac4c --- /dev/null +++ b/Linphone/core/singleapplication/README.md @@ -0,0 +1,10 @@ +Coming from https://github.com/itay-grudev/SingleApplication + +File format are kept from initial repository : +- singleapplication.cpp +- singleapplication.h +- singleapplication_p.cpp +- singleapplication_p.h + +Support for Linux has been added with DBus. + diff --git a/Linphone/core/singleapplication/SingleApplicationDBus.cpp b/Linphone/core/singleapplication/SingleApplicationDBus.cpp new file mode 100644 index 000000000..a99095b36 --- /dev/null +++ b/Linphone/core/singleapplication/SingleApplicationDBus.cpp @@ -0,0 +1,128 @@ +/* + * 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 "SingleApplicationDBusPrivate.hpp" +#include "singleapplication.h" + +// ============================================================================= + +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, const QString &userData) + : 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() const { + auto d = d_func(); + return d->instanceNumber == 0; +} + +bool SingleApplication::isSecondary() const { + auto d = d_func(); + return d->instanceNumber != 0; +} + +quint32 SingleApplication::instanceId() const { + auto d = d_func(); + return d->instanceNumber; +} + +bool SingleApplication::sendMessage(const QByteArray &message, int timeout, SendMode sendMode) { + auto d = d_func(); + + 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); +} diff --git a/Linphone/core/singleapplication/SingleApplicationDBusPrivate.hpp b/Linphone/core/singleapplication/SingleApplicationDBusPrivate.hpp new file mode 100644 index 000000000..b3708afed --- /dev/null +++ b/Linphone/core/singleapplication/SingleApplicationDBusPrivate.hpp @@ -0,0 +1,62 @@ +/* + * 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.h" + +// ============================================================================= + +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/core/singleapplication/singleapplication.cpp b/Linphone/core/singleapplication/singleapplication.cpp new file mode 100644 index 000000000..b8b21049b --- /dev/null +++ b/Linphone/core/singleapplication/singleapplication.cpp @@ -0,0 +1,262 @@ +// 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/core/singleapplication/singleapplication.h b/Linphone/core/singleapplication/singleapplication.h new file mode 100644 index 000000000..fafb8db9a --- /dev/null +++ b/Linphone/core/singleapplication/singleapplication.h @@ -0,0 +1,192 @@ +// 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 +#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; + +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/core/singleapplication/singleapplication_p.cpp b/Linphone/core/singleapplication/singleapplication_p.cpp new file mode 100644 index 000000000..03d6e7bb5 --- /dev/null +++ b/Linphone/core/singleapplication/singleapplication_p.cpp @@ -0,0 +1,510 @@ +// 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/core/singleapplication/singleapplication_p.h b/Linphone/core/singleapplication/singleapplication_p.h new file mode 100644 index 000000000..e0c1ba032 --- /dev/null +++ b/Linphone/core/singleapplication/singleapplication_p.h @@ -0,0 +1,107 @@ +// 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 "singleapplication.h" +#include +#include +#include + +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 diff --git a/Linphone/main.cpp b/Linphone/main.cpp index 774873b2d..53d9109e8 100644 --- a/Linphone/main.cpp +++ b/Linphone/main.cpp @@ -7,7 +7,7 @@ #include "core/App.hpp" int main(int argc, char *argv[]) { - QApplication app(argc, argv); + App app(argc, argv); QTranslator translator; const QStringList uiLanguages = QLocale::system().uiLanguages(); @@ -18,7 +18,7 @@ int main(int argc, char *argv[]) { break; } } - App a; + int result = app.exec(); return result; } diff --git a/Linphone/model/CMakeLists.txt b/Linphone/model/CMakeLists.txt index 88b0f0329..1cae29160 100644 --- a/Linphone/model/CMakeLists.txt +++ b/Linphone/model/CMakeLists.txt @@ -4,7 +4,6 @@ list(APPEND _LINPHONEAPP_SOURCES model/logger/LoggerModel.cpp model/logger/LoggerListener.cpp - model/logger/LoggerStaticModel.cpp model/setting/SettingsModel.cpp ) diff --git a/Linphone/model/logger/LoggerListener.cpp b/Linphone/model/logger/LoggerListener.cpp index 337b9707d..6481fe1fd 100644 --- a/Linphone/model/logger/LoggerListener.cpp +++ b/Linphone/model/logger/LoggerListener.cpp @@ -62,7 +62,7 @@ void LoggerListener::onLogMessageWritten(const std::shared_ptr &logService, const std::string &domain, linphone::LogLevel level, const std::string &message) override; - - bool mIsVerbose = true; - bool mQtOnlyEnabled = false; }; #endif diff --git a/Linphone/model/logger/LoggerModel.cpp b/Linphone/model/logger/LoggerModel.cpp index 2a467656c..1c1c8c9c5 100644 --- a/Linphone/model/logger/LoggerModel.cpp +++ b/Linphone/model/logger/LoggerModel.cpp @@ -31,11 +31,14 @@ #include "tool/Constants.hpp" #include "tool/Utils.hpp" -#include "LoggerStaticModel.hpp" +#include "core/logger/QtLogger.hpp" // ----------------------------------------------------------------------------- LoggerModel::LoggerModel(QObject *parent) : QObject(parent) { - connect(LoggerStaticModel::getInstance(), &LoggerStaticModel::logReceived, this, &LoggerModel::onLog, + connect(QtLogger::getInstance(), &QtLogger::logReceived, this, &LoggerModel::onLog, Qt::QueuedConnection); + connect(QtLogger::getInstance(), &QtLogger::requestVerboseEnabled, this, &LoggerModel::enableVerbose, + Qt::QueuedConnection); + connect(QtLogger::getInstance(), &QtLogger::requestQtOnlyEnabled, this, &LoggerModel::enableQtOnly, Qt::QueuedConnection); } @@ -43,11 +46,34 @@ LoggerModel::~LoggerModel() { } bool LoggerModel::isVerbose() const { - return mVerbose; + return mListener ? mListener->mVerboseEnabled : false; } -void LoggerModel::setVerbose(bool verbose) { - mVerbose = verbose; +void LoggerModel::enableVerbose(bool verbose) { + if (mListener && mListener->mVerboseEnabled != verbose) { + mListener->mVerboseEnabled = verbose; + emit verboseEnabledChanged(); + } +} + +void LoggerModel::enableFullLogs(const bool &full) { + auto service = linphone::LoggingService::get(); + if (service) service->setLogLevel(full ? linphone::LogLevel::Debug : linphone::LogLevel::Message); +} + +bool LoggerModel::qtOnlyEnabled() const { + return mListener ? mListener->mQtOnlyEnabled : false; +} + +void LoggerModel::enableQtOnly(const bool &enable) { + if (mListener) { + if (mListener->mQtOnlyEnabled != enable) { + mListener->mQtOnlyEnabled = enable; + auto service = linphone::LoggingService::get(); + if (service) service->setDomain(enable ? Constants::AppDomain : ""); + emit qtOnlyEnabledChanged(); + } + } } // ----------------------------------------------------------------------------- @@ -124,25 +150,6 @@ void LoggerModel::init() { enable(true); } -void LoggerModel::enableFullLogs(const bool &full) { - auto service = linphone::LoggingService::get(); - if (service) { - service->setLogLevel(full ? linphone::LogLevel::Debug : linphone::LogLevel::Message); - } -} - -bool LoggerModel::qtOnlyEnabled() const { - return mQtOnly; -} - -void LoggerModel::enableQtOnly(const bool &enable) { - mQtOnly = enable; - auto service = linphone::LoggingService::get(); - if (service) { - service->setDomain(enable ? Constants::AppDomain : ""); - } -} - QString LoggerModel::getLogText() const { QDir path = QString::fromStdString(linphone::Core::getLogCollectionPath()); QString prefix = QString::fromStdString(linphone::Core::getLogCollectionPrefix()); diff --git a/Linphone/model/logger/LoggerModel.hpp b/Linphone/model/logger/LoggerModel.hpp index 9bbba2aa9..03a1b8809 100644 --- a/Linphone/model/logger/LoggerModel.hpp +++ b/Linphone/model/logger/LoggerModel.hpp @@ -35,7 +35,7 @@ public: ~LoggerModel(); bool isVerbose() const; - void setVerbose(bool verbose); + void enableVerbose(bool verbose); void enable(bool status); QString getLogText() const; void enableFullLogs(const bool &full); @@ -51,13 +51,11 @@ public slots: signals: void logReceived(QtMsgType type, QString contextFile, int contextLine, QString msg); + void verboseEnabledChanged(); + void qtOnlyEnabledChanged(); private: static void log(QtMsgType type, const QMessageLogContext &context, const QString &msg); - - bool mVerbose = false; - bool mQtOnly = false; - std::shared_ptr mListener; }; diff --git a/external/linphone-sdk b/external/linphone-sdk index 521a001ab..949394d21 160000 --- a/external/linphone-sdk +++ b/external/linphone-sdk @@ -1 +1 @@ -Subproject commit 521a001ab21f1127682333efce8d4a292099203c +Subproject commit 949394d2158c44ddad4725327dc65e4488d36581