mirror of
https://gitlab.linphone.org/BC/public/linphone-desktop.git
synced 2026-01-17 03:18:07 +00:00
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.
This commit is contained in:
parent
21f002105f
commit
b6ff625370
20 changed files with 1400 additions and 54 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -22,13 +22,16 @@
|
|||
|
||||
#include <QCoreApplication>
|
||||
|
||||
#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<CoreModel>::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")},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,16 @@
|
|||
*/
|
||||
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QCommandLineParser>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#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<CoreModel> mCoreModel;
|
||||
};
|
||||
|
||||
private:
|
||||
void createCommandParser();
|
||||
|
||||
QCommandLineParser *mParser = nullptr;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -18,20 +18,28 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "LoggerStaticModel.hpp"
|
||||
#include "QtLogger.hpp"
|
||||
#include <QMetaMethod>
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -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
|
||||
10
Linphone/core/singleapplication/README.md
Normal file
10
Linphone/core/singleapplication/README.md
Normal file
|
|
@ -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.
|
||||
|
||||
128
Linphone/core/singleapplication/SingleApplicationDBus.cpp
Normal file
128
Linphone/core/singleapplication/SingleApplicationDBus.cpp
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
#include <QDBusInterface>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SINGLE_APPLICATION_DBUS_PRIVATE_H_
|
||||
#define SINGLE_APPLICATION_DBUS_PRIVATE_H_
|
||||
|
||||
#include <QDBusAbstractAdaptor>
|
||||
#include <QDBusConnection>
|
||||
|
||||
#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_
|
||||
262
Linphone/core/singleapplication/singleapplication.cpp
Normal file
262
Linphone/core/singleapplication/singleapplication.cpp
Normal file
|
|
@ -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 <QtCore/QByteArray>
|
||||
#include <QtCore/QElapsedTimer>
|
||||
#include <QtCore/QSharedMemory>
|
||||
|
||||
#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<InstancesInfo *>(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();
|
||||
}
|
||||
192
Linphone/core/singleapplication/singleapplication.h
Normal file
192
Linphone/core/singleapplication/singleapplication.h
Normal file
|
|
@ -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 <QObject>
|
||||
#include <QtCore/QtGlobal>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
|
||||
#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
|
||||
510
Linphone/core/singleapplication/singleapplication_p.cpp
Normal file
510
Linphone/core/singleapplication/singleapplication_p.cpp
Normal file
|
|
@ -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 <cstddef>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QCryptographicHash>
|
||||
#include <QtCore/QDataStream>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QElapsedTimer>
|
||||
#include <QtCore/QThread>
|
||||
#include <QtNetwork/QLocalServer>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
#include <QtCore/QRandomGenerator>
|
||||
#else
|
||||
#include <QtCore/QDateTime>
|
||||
#endif
|
||||
|
||||
#include "singleapplication.h"
|
||||
#include "singleapplication_p.h"
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX 1
|
||||
#endif
|
||||
#include <lmcons.h>
|
||||
#include <windows.h>
|
||||
#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<InstancesInfo *>(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<InstancesInfo *>(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<InstancesInfo *>(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<InstancesInfo *>(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<int>(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<quint8>(connectionType);
|
||||
writeStream << instanceNumber;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
quint16 checksum = qChecksum(QByteArray(initMsg.constData(), static_cast<quint32>(initMsg.length())));
|
||||
#else
|
||||
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
|
||||
#endif
|
||||
writeStream << checksum;
|
||||
|
||||
return writeConfirmedMessage(static_cast<int>(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<quint64>(msg.length());
|
||||
|
||||
if (!writeConfirmedFrame(static_cast<int>(msecs - time.elapsed()), header)) return false;
|
||||
|
||||
// Frame 2: The message
|
||||
const bool result = writeConfirmedFrame(static_cast<int>(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<const char *>(memory->constData()), offsetof(InstancesInfo, checksum)));
|
||||
#else
|
||||
quint16 checksum = qChecksum(static_cast<const char *>(memory->constData()), offsetof(InstancesInfo, checksum));
|
||||
#endif
|
||||
return checksum;
|
||||
}
|
||||
|
||||
qint64 SingleApplicationPrivate::primaryPid() const {
|
||||
qint64 pid;
|
||||
|
||||
memory->lock();
|
||||
auto *inst = static_cast<InstancesInfo *>(memory->data());
|
||||
pid = inst->primaryPid;
|
||||
memory->unlock();
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
QString SingleApplicationPrivate::primaryUser() const {
|
||||
QByteArray username;
|
||||
|
||||
memory->lock();
|
||||
auto *inst = static_cast<InstancesInfo *>(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<ConnectionType>(connTypeVal);
|
||||
|
||||
// instance id
|
||||
quint32 instanceId = 0;
|
||||
readStream >> instanceId;
|
||||
|
||||
// checksum
|
||||
quint16 msgChecksum = 0;
|
||||
readStream >> msgChecksum;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
const quint16 actualChecksum =
|
||||
qChecksum(QByteArray(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16))));
|
||||
#else
|
||||
const quint16 actualChecksum =
|
||||
qChecksum(msgBytes.constData(), static_cast<quint32>(msgBytes.length() - sizeof(quint16)));
|
||||
#endif
|
||||
|
||||
bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName &&
|
||||
msgChecksum == actualChecksum;
|
||||
|
||||
if (!isValid) {
|
||||
sock->close();
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectionInfo &info = connectionMap[sock];
|
||||
info.instanceId = instanceId;
|
||||
info.stage = StageConnectedHeader;
|
||||
|
||||
if (connectionType == NewInstance ||
|
||||
(connectionType == SecondaryInstance && options & 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<uint>::max());
|
||||
QThread::msleep(qrand() % 11 + 8);
|
||||
#endif
|
||||
}
|
||||
|
||||
void SingleApplicationPrivate::addAppData(const QString &data) {
|
||||
appDataList.push_back(data);
|
||||
}
|
||||
|
||||
QStringList SingleApplicationPrivate::appData() const {
|
||||
return appDataList;
|
||||
}
|
||||
107
Linphone/core/singleapplication/singleapplication_p.h
Normal file
107
Linphone/core/singleapplication/singleapplication_p.h
Normal file
|
|
@ -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 <QtCore/QSharedMemory>
|
||||
#include <QtNetwork/QLocalServer>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
|
||||
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<QLocalSocket *, ConnectionInfo> connectionMap;
|
||||
QStringList appDataList;
|
||||
|
||||
public Q_SLOTS:
|
||||
void slotConnectionEstablished();
|
||||
void slotDataAvailable(QLocalSocket *, quint32);
|
||||
void slotClientConnectionClosed(QLocalSocket *, quint32);
|
||||
};
|
||||
|
||||
#endif // SINGLEAPPLICATION_P_H
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ list(APPEND _LINPHONEAPP_SOURCES
|
|||
|
||||
model/logger/LoggerModel.cpp
|
||||
model/logger/LoggerListener.cpp
|
||||
model/logger/LoggerStaticModel.cpp
|
||||
|
||||
model/setting/SettingsModel.cpp
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ void LoggerListener::onLogMessageWritten(const std::shared_ptr<linphone::Logging
|
|||
linphone::LogLevel level,
|
||||
const std::string &message) {
|
||||
bool isAppLog = domain == Constants::AppDomain;
|
||||
if (!mIsVerbose || (!isAppLog && mQtOnlyEnabled)) return;
|
||||
if (!mVerboseEnabled || (!isAppLog && mQtOnlyEnabled)) return;
|
||||
FILE *out = stdout;
|
||||
// TypeColor Date SourceColor [Domain] TypeColor Type Reset Message
|
||||
QString format = "%1 %2 %3[%4]%1 %5" RESET " %6\n";
|
||||
|
|
|
|||
|
|
@ -30,15 +30,15 @@
|
|||
class LoggerListener : public linphone::LoggingServiceListener {
|
||||
public:
|
||||
LoggerListener();
|
||||
|
||||
bool mVerboseEnabled = false;
|
||||
bool mQtOnlyEnabled = false;
|
||||
|
||||
private:
|
||||
virtual void onLogMessageWritten(const std::shared_ptr<linphone::LoggingService> &logService,
|
||||
const std::string &domain,
|
||||
linphone::LogLevel level,
|
||||
const std::string &message) override;
|
||||
|
||||
bool mIsVerbose = true;
|
||||
bool mQtOnlyEnabled = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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<LoggerListener> mListener;
|
||||
};
|
||||
|
||||
|
|
|
|||
2
external/linphone-sdk
vendored
2
external/linphone-sdk
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 521a001ab21f1127682333efce8d4a292099203c
|
||||
Subproject commit 949394d2158c44ddad4725327dc65e4488d36581
|
||||
Loading…
Add table
Reference in a new issue