mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-24 03:50:59 +03:00
Switch profile code to a more flexible method
This introduces a new version of SingleApplication as well.
This commit is contained in:
parent
100b5e0371
commit
53f45bdb1c
21 changed files with 654 additions and 373 deletions
|
@ -449,7 +449,7 @@ pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.14 gstreamer-we
|
||||||
|
|
||||||
# single instance functionality
|
# single instance functionality
|
||||||
set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
|
set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
|
||||||
add_subdirectory(third_party/SingleApplication-3.1.3.1/)
|
add_subdirectory(third_party/SingleApplication-3.2.0-dc8042b/)
|
||||||
|
|
||||||
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ Specifically there is support for:
|
||||||
- Basic communities support.
|
- Basic communities support.
|
||||||
- Room switcher (ctrl-K).
|
- Room switcher (ctrl-K).
|
||||||
- Light, Dark & System themes.
|
- Light, Dark & System themes.
|
||||||
- Creating separate profiles (command line only, use `--profile=name`).
|
- Creating separate profiles (command line only, use `-p name`).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QCryptographicHash>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MatrixClient.h"
|
#include "MatrixClient.h"
|
||||||
#include "Olm.h"
|
#include "Olm.h"
|
||||||
|
#include "UserSettingsPage.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
//! Should be changed when a breaking change occurs in the cache format.
|
//! Should be changed when a breaking change occurs in the cache format.
|
||||||
|
@ -165,17 +167,15 @@ Cache::Cache(const QString &userId, QObject *parent)
|
||||||
void
|
void
|
||||||
Cache::setup()
|
Cache::setup()
|
||||||
{
|
{
|
||||||
|
UserSettings settings;
|
||||||
|
|
||||||
nhlog::db()->debug("setting up cache");
|
nhlog::db()->debug("setting up cache");
|
||||||
|
|
||||||
auto statePath = QString("%1/%2")
|
cacheDirectory_ = QString("%1/%2%3")
|
||||||
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
|
|
||||||
.arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
|
|
||||||
|
|
||||||
cacheDirectory_ = QString("%1/%2")
|
|
||||||
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
|
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
|
||||||
.arg(QString::fromUtf8(localUserId_.toUtf8().toHex()));
|
.arg(QString::fromUtf8(localUserId_.toUtf8().toHex())).arg(QString::fromUtf8(settings.profile().toUtf8().toHex()));
|
||||||
|
|
||||||
bool isInitial = !QFile::exists(statePath);
|
bool isInitial = !QFile::exists(cacheDirectory_);
|
||||||
|
|
||||||
env_ = lmdb::env::create();
|
env_ = lmdb::env::create();
|
||||||
env_.set_mapsize(DB_SIZE);
|
env_.set_mapsize(DB_SIZE);
|
||||||
|
@ -184,9 +184,9 @@ Cache::setup()
|
||||||
if (isInitial) {
|
if (isInitial) {
|
||||||
nhlog::db()->info("initializing LMDB");
|
nhlog::db()->info("initializing LMDB");
|
||||||
|
|
||||||
if (!QDir().mkpath(statePath)) {
|
if (!QDir().mkpath(cacheDirectory_)) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
("Unable to create state directory:" + statePath).toStdString().c_str());
|
("Unable to create state directory:" + cacheDirectory_).toStdString().c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ Cache::setup()
|
||||||
// NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but
|
// NOTE(Nico): We may want to use (MDB_MAPASYNC | MDB_WRITEMAP) in the future, but
|
||||||
// it can really mess up our database, so we shouldn't. For now, hopefully
|
// it can really mess up our database, so we shouldn't. For now, hopefully
|
||||||
// NOMETASYNC is fast enough.
|
// NOMETASYNC is fast enough.
|
||||||
env_.open(statePath.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC);
|
env_.open(cacheDirectory_.toStdString().c_str(), MDB_NOMETASYNC | MDB_NOSYNC);
|
||||||
} catch (const lmdb::error &e) {
|
} catch (const lmdb::error &e) {
|
||||||
if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
|
if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) {
|
||||||
throw std::runtime_error("LMDB initialization failed" +
|
throw std::runtime_error("LMDB initialization failed" +
|
||||||
|
@ -203,15 +203,14 @@ Cache::setup()
|
||||||
|
|
||||||
nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
|
nhlog::db()->warn("resetting cache due to LMDB version mismatch: {}", e.what());
|
||||||
|
|
||||||
QDir stateDir(statePath);
|
QDir stateDir(cacheDirectory_);
|
||||||
|
|
||||||
for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
|
for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) {
|
||||||
if (!stateDir.remove(file))
|
if (!stateDir.remove(file))
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
("Unable to delete file " + file).toStdString().c_str());
|
("Unable to delete file " + file).toStdString().c_str());
|
||||||
}
|
}
|
||||||
|
env_.open(cacheDirectory_.toStdString().c_str());
|
||||||
env_.open(statePath.toStdString().c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto txn = lmdb::txn::begin(env_);
|
auto txn = lmdb::txn::begin(env_);
|
||||||
|
@ -577,10 +576,14 @@ Cache::restoreOlmAccount()
|
||||||
void
|
void
|
||||||
Cache::storeSecret(const std::string &name, const std::string &secret)
|
Cache::storeSecret(const std::string &name, const std::string &secret)
|
||||||
{
|
{
|
||||||
|
UserSettings settings;
|
||||||
QKeychain::WritePasswordJob job(QCoreApplication::applicationName());
|
QKeychain::WritePasswordJob job(QCoreApplication::applicationName());
|
||||||
job.setAutoDelete(false);
|
job.setAutoDelete(false);
|
||||||
job.setInsecureFallback(true);
|
job.setInsecureFallback(true);
|
||||||
job.setKey(QString::fromStdString(name));
|
job.setKey(
|
||||||
|
"matrix." +
|
||||||
|
QString(QCryptographicHash::hash(settings.profile().toUtf8(), QCryptographicHash::Sha256)) +
|
||||||
|
"." + name.c_str());
|
||||||
job.setTextData(QString::fromStdString(secret));
|
job.setTextData(QString::fromStdString(secret));
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||||
|
@ -598,10 +601,14 @@ Cache::storeSecret(const std::string &name, const std::string &secret)
|
||||||
void
|
void
|
||||||
Cache::deleteSecret(const std::string &name)
|
Cache::deleteSecret(const std::string &name)
|
||||||
{
|
{
|
||||||
|
UserSettings settings;
|
||||||
QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
|
QKeychain::DeletePasswordJob job(QCoreApplication::applicationName());
|
||||||
job.setAutoDelete(false);
|
job.setAutoDelete(false);
|
||||||
job.setInsecureFallback(true);
|
job.setInsecureFallback(true);
|
||||||
job.setKey(QString::fromStdString(name));
|
job.setKey(
|
||||||
|
"matrix." +
|
||||||
|
QString(QCryptographicHash::hash(settings.profile().toUtf8(), QCryptographicHash::Sha256)) +
|
||||||
|
"." + name.c_str());
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||||
job.start();
|
job.start();
|
||||||
|
@ -613,10 +620,14 @@ Cache::deleteSecret(const std::string &name)
|
||||||
std::optional<std::string>
|
std::optional<std::string>
|
||||||
Cache::secret(const std::string &name)
|
Cache::secret(const std::string &name)
|
||||||
{
|
{
|
||||||
|
UserSettings settings;
|
||||||
QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
|
QKeychain::ReadPasswordJob job(QCoreApplication::applicationName());
|
||||||
job.setAutoDelete(false);
|
job.setAutoDelete(false);
|
||||||
job.setInsecureFallback(true);
|
job.setInsecureFallback(true);
|
||||||
job.setKey(QString::fromStdString(name));
|
job.setKey(
|
||||||
|
"matrix." +
|
||||||
|
QString(QCryptographicHash::hash(settings.profile().toUtf8(), QCryptographicHash::Sha256)) +
|
||||||
|
"." + name.c_str());
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||||
job.start();
|
job.start();
|
||||||
|
|
|
@ -55,9 +55,9 @@
|
||||||
|
|
||||||
MainWindow *MainWindow::instance_ = nullptr;
|
MainWindow *MainWindow::instance_ = nullptr;
|
||||||
|
|
||||||
MainWindow::MainWindow(const QString profile, QWidget *parent)
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
: QMainWindow(parent)
|
: QMainWindow(parent),
|
||||||
, profile_{profile}
|
userSettings_{QSharedPointer<UserSettings>{new UserSettings}}
|
||||||
{
|
{
|
||||||
setWindowTitle(0);
|
setWindowTitle(0);
|
||||||
setObjectName("MainWindow");
|
setObjectName("MainWindow");
|
||||||
|
@ -70,8 +70,7 @@ MainWindow::MainWindow(const QString profile, QWidget *parent)
|
||||||
font.setStyleStrategy(QFont::PreferAntialias);
|
font.setStyleStrategy(QFont::PreferAntialias);
|
||||||
setFont(font);
|
setFont(font);
|
||||||
|
|
||||||
userSettings_ = QSharedPointer<UserSettings>(new UserSettings);
|
trayIcon_ = new TrayIcon(":/logos/nheko.svg", this);
|
||||||
trayIcon_ = new TrayIcon(":/logos/nheko.svg", this);
|
|
||||||
|
|
||||||
welcome_page_ = new WelcomePage(this);
|
welcome_page_ = new WelcomePage(this);
|
||||||
login_page_ = new LoginPage(this);
|
login_page_ = new LoginPage(this);
|
||||||
|
@ -150,15 +149,13 @@ MainWindow::MainWindow(const QString profile, QWidget *parent)
|
||||||
chat_page_->showQuickSwitcher();
|
chat_page_->showQuickSwitcher();
|
||||||
});
|
});
|
||||||
|
|
||||||
QSettings settings;
|
|
||||||
|
|
||||||
trayIcon_->setVisible(userSettings_->tray());
|
trayIcon_->setVisible(userSettings_->tray());
|
||||||
|
|
||||||
if (hasActiveUser()) {
|
if (hasActiveUser()) {
|
||||||
QString token = settings.value("auth/access_token").toString();
|
QString token = userSettings_->accessToken();
|
||||||
QString home_server = settings.value("auth/home_server").toString();
|
QString home_server = userSettings_->homeserver();
|
||||||
QString user_id = settings.value("auth/user_id").toString();
|
QString user_id = userSettings_->userId();
|
||||||
QString device_id = settings.value("auth/device_id").toString();
|
QString device_id = userSettings_->deviceId();
|
||||||
|
|
||||||
http::client()->set_access_token(token.toStdString());
|
http::client()->set_access_token(token.toStdString());
|
||||||
http::client()->set_server(home_server.toStdString());
|
http::client()->set_server(home_server.toStdString());
|
||||||
|
@ -184,8 +181,9 @@ void
|
||||||
MainWindow::setWindowTitle(int notificationCount)
|
MainWindow::setWindowTitle(int notificationCount)
|
||||||
{
|
{
|
||||||
QString name = "nheko";
|
QString name = "nheko";
|
||||||
if (!profile_.isEmpty())
|
|
||||||
name += " | " + profile_;
|
if (!userSettings_.data()->profile().isEmpty())
|
||||||
|
name += " | " + userSettings_.data()->profile();
|
||||||
if (notificationCount > 0) {
|
if (notificationCount > 0) {
|
||||||
name.append(QString{" (%1)"}.arg(notificationCount));
|
name.append(QString{" (%1)"}.arg(notificationCount));
|
||||||
}
|
}
|
||||||
|
@ -279,11 +277,10 @@ MainWindow::showChatPage()
|
||||||
std::to_string(http::client()->port()));
|
std::to_string(http::client()->port()));
|
||||||
auto token = QString::fromStdString(http::client()->access_token());
|
auto token = QString::fromStdString(http::client()->access_token());
|
||||||
|
|
||||||
QSettings settings;
|
userSettings_.data()->setUserId(userid);
|
||||||
settings.setValue("auth/access_token", token);
|
userSettings_.data()->setAccessToken(token);
|
||||||
settings.setValue("auth/home_server", homeserver);
|
userSettings_.data()->setDeviceId(device_id);
|
||||||
settings.setValue("auth/user_id", userid);
|
userSettings_.data()->setHomeserver(homeserver);
|
||||||
settings.setValue("auth/device_id", device_id);
|
|
||||||
|
|
||||||
showOverlayProgressBar();
|
showOverlayProgressBar();
|
||||||
|
|
||||||
|
@ -341,9 +338,13 @@ bool
|
||||||
MainWindow::hasActiveUser()
|
MainWindow::hasActiveUser()
|
||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
QString prefix;
|
||||||
|
if (userSettings_->profile() != "")
|
||||||
|
prefix = "profile/" + userSettings_->profile() + "/";
|
||||||
|
|
||||||
return settings.contains("auth/access_token") && settings.contains("auth/home_server") &&
|
return settings.contains(prefix + "auth/access_token") &&
|
||||||
settings.contains("auth/user_id");
|
settings.contains(prefix + "auth/home_server") &&
|
||||||
|
settings.contains(prefix + "auth/user_id");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
@ -62,7 +62,7 @@ class MainWindow : public QMainWindow
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MainWindow(const QString name, QWidget *parent = nullptr);
|
explicit MainWindow(QWidget *parent = nullptr);
|
||||||
|
|
||||||
static MainWindow *instance() { return instance_; };
|
static MainWindow *instance() { return instance_; };
|
||||||
void saveCurrentWindowSize();
|
void saveCurrentWindowSize();
|
||||||
|
@ -149,6 +149,4 @@ private:
|
||||||
LoadingIndicator *spinner_ = nullptr;
|
LoadingIndicator *spinner_ = nullptr;
|
||||||
|
|
||||||
JdenticonInterface *jdenticonInteface_ = nullptr;
|
JdenticonInterface *jdenticonInteface_ = nullptr;
|
||||||
|
|
||||||
QString profile_;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -89,6 +89,14 @@ UserSettings::load()
|
||||||
cameraResolution_ = settings.value("user/camera_resolution", QString()).toString();
|
cameraResolution_ = settings.value("user/camera_resolution", QString()).toString();
|
||||||
cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString();
|
cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString();
|
||||||
useStunServer_ = settings.value("user/use_stun_server", false).toBool();
|
useStunServer_ = settings.value("user/use_stun_server", false).toBool();
|
||||||
|
profile_ = settings.value("user/currentProfile", "").toString();
|
||||||
|
|
||||||
|
QString prefix =
|
||||||
|
(profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : "";
|
||||||
|
accessToken_ = settings.value(prefix + "auth/access_token", "").toString();
|
||||||
|
homeserver_ = settings.value(prefix + "auth/home_server", "").toString();
|
||||||
|
userId_ = settings.value(prefix + "auth/user_id", "").toString();
|
||||||
|
deviceId_ = settings.value(prefix + "auth/device_id", "").toString();
|
||||||
|
|
||||||
applyTheme();
|
applyTheme();
|
||||||
}
|
}
|
||||||
|
@ -372,6 +380,56 @@ UserSettings::setCameraFrameRate(QString frameRate)
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserSettings::setProfile(QString profile)
|
||||||
|
{
|
||||||
|
if (profile == profile_)
|
||||||
|
return;
|
||||||
|
profile_ = profile;
|
||||||
|
emit profileChanged(profile_);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserSettings::setUserId(QString userId)
|
||||||
|
{
|
||||||
|
if (userId == userId_)
|
||||||
|
return;
|
||||||
|
userId_ = userId;
|
||||||
|
emit userIdChanged(userId_);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserSettings::setAccessToken(QString accessToken)
|
||||||
|
{
|
||||||
|
if (accessToken == accessToken_)
|
||||||
|
return;
|
||||||
|
accessToken_ = accessToken;
|
||||||
|
emit accessTokenChanged(accessToken_);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserSettings::setDeviceId(QString deviceId)
|
||||||
|
{
|
||||||
|
if (deviceId == deviceId_)
|
||||||
|
return;
|
||||||
|
deviceId_ = deviceId;
|
||||||
|
emit deviceIdChanged(deviceId_);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
UserSettings::setHomeserver(QString homeserver)
|
||||||
|
{
|
||||||
|
if (homeserver == homeserver_)
|
||||||
|
return;
|
||||||
|
homeserver_ = homeserver;
|
||||||
|
emit homeserverChanged(homeserver_);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
UserSettings::applyTheme()
|
UserSettings::applyTheme()
|
||||||
{
|
{
|
||||||
|
@ -436,14 +494,14 @@ UserSettings::save()
|
||||||
settings.beginGroup("window");
|
settings.beginGroup("window");
|
||||||
settings.setValue("tray", tray_);
|
settings.setValue("tray", tray_);
|
||||||
settings.setValue("start_in_tray", startInTray_);
|
settings.setValue("start_in_tray", startInTray_);
|
||||||
settings.endGroup();
|
settings.endGroup(); // window
|
||||||
|
|
||||||
settings.beginGroup("timeline");
|
settings.beginGroup("timeline");
|
||||||
settings.setValue("buttons", buttonsInTimeline_);
|
settings.setValue("buttons", buttonsInTimeline_);
|
||||||
settings.setValue("message_hover_highlight", messageHoverHighlight_);
|
settings.setValue("message_hover_highlight", messageHoverHighlight_);
|
||||||
settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_);
|
settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_);
|
||||||
settings.setValue("max_width", timelineMaxWidth_);
|
settings.setValue("max_width", timelineMaxWidth_);
|
||||||
settings.endGroup();
|
settings.endGroup(); // timeline
|
||||||
|
|
||||||
settings.setValue("avatar_circles", avatarCircles_);
|
settings.setValue("avatar_circles", avatarCircles_);
|
||||||
settings.setValue("decrypt_sidebar", decryptSidebar_);
|
settings.setValue("decrypt_sidebar", decryptSidebar_);
|
||||||
|
@ -467,8 +525,16 @@ UserSettings::save()
|
||||||
settings.setValue("camera_resolution", cameraResolution_);
|
settings.setValue("camera_resolution", cameraResolution_);
|
||||||
settings.setValue("camera_frame_rate", cameraFrameRate_);
|
settings.setValue("camera_frame_rate", cameraFrameRate_);
|
||||||
settings.setValue("use_stun_server", useStunServer_);
|
settings.setValue("use_stun_server", useStunServer_);
|
||||||
|
settings.setValue("currentProfile", profile_);
|
||||||
|
|
||||||
settings.endGroup();
|
QString prefix =
|
||||||
|
(profile_ != "" && profile_ != "default") ? "profile/" + profile_ + "/" : "";
|
||||||
|
settings.setValue(prefix + "auth/access_token", accessToken_);
|
||||||
|
settings.setValue(prefix + "auth/home_server", homeserver_);
|
||||||
|
settings.setValue(prefix + "auth/user_id", userId_);
|
||||||
|
settings.setValue(prefix + "auth/device_id", deviceId_);
|
||||||
|
|
||||||
|
settings.endGroup(); // user
|
||||||
|
|
||||||
settings.sync();
|
settings.sync();
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,12 @@ class UserSettings : public QObject
|
||||||
bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
|
bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
|
||||||
Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
|
Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE
|
||||||
setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
|
setShareKeysWithTrustedUsers NOTIFY shareKeysWithTrustedUsersChanged)
|
||||||
|
Q_PROPERTY(QString profile READ profile WRITE setProfile NOTIFY profileChanged)
|
||||||
|
Q_PROPERTY(QString userId READ userId WRITE setUserId NOTIFY userIdChanged)
|
||||||
|
Q_PROPERTY(
|
||||||
|
QString accessToken READ accessToken WRITE setAccessToken NOTIFY accessTokenChanged)
|
||||||
|
Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged)
|
||||||
|
Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
UserSettings();
|
UserSettings();
|
||||||
|
@ -95,7 +101,7 @@ public:
|
||||||
Unavailable,
|
Unavailable,
|
||||||
Offline,
|
Offline,
|
||||||
};
|
};
|
||||||
Q_ENUM(Presence);
|
Q_ENUM(Presence)
|
||||||
|
|
||||||
void save();
|
void save();
|
||||||
void load();
|
void load();
|
||||||
|
@ -128,6 +134,11 @@ public:
|
||||||
void setCameraFrameRate(QString frameRate);
|
void setCameraFrameRate(QString frameRate);
|
||||||
void setUseStunServer(bool state);
|
void setUseStunServer(bool state);
|
||||||
void setShareKeysWithTrustedUsers(bool state);
|
void setShareKeysWithTrustedUsers(bool state);
|
||||||
|
void setProfile(QString profile);
|
||||||
|
void setUserId(QString userId);
|
||||||
|
void setAccessToken(QString accessToken);
|
||||||
|
void setDeviceId(QString deviceId);
|
||||||
|
void setHomeserver(QString homeserver);
|
||||||
|
|
||||||
QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
|
QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
|
||||||
bool messageHoverHighlight() const { return messageHoverHighlight_; }
|
bool messageHoverHighlight() const { return messageHoverHighlight_; }
|
||||||
|
@ -161,6 +172,11 @@ public:
|
||||||
QString cameraFrameRate() const { return cameraFrameRate_; }
|
QString cameraFrameRate() const { return cameraFrameRate_; }
|
||||||
bool useStunServer() const { return useStunServer_; }
|
bool useStunServer() const { return useStunServer_; }
|
||||||
bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
|
bool shareKeysWithTrustedUsers() const { return shareKeysWithTrustedUsers_; }
|
||||||
|
QString profile() const { return profile_; }
|
||||||
|
QString userId() const { return userId_; }
|
||||||
|
QString accessToken() const { return accessToken_; }
|
||||||
|
QString deviceId() const { return deviceId_; }
|
||||||
|
QString homeserver() const { return homeserver_; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void groupViewStateChanged(bool state);
|
void groupViewStateChanged(bool state);
|
||||||
|
@ -191,6 +207,11 @@ signals:
|
||||||
void cameraFrameRateChanged(QString frameRate);
|
void cameraFrameRateChanged(QString frameRate);
|
||||||
void useStunServerChanged(bool state);
|
void useStunServerChanged(bool state);
|
||||||
void shareKeysWithTrustedUsersChanged(bool state);
|
void shareKeysWithTrustedUsersChanged(bool state);
|
||||||
|
void profileChanged(QString profile);
|
||||||
|
void userIdChanged(QString userId);
|
||||||
|
void accessTokenChanged(QString accessToken);
|
||||||
|
void deviceIdChanged(QString deviceId);
|
||||||
|
void homeserverChanged(QString homeserver);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Default to system theme if QT_QPA_PLATFORMTHEME var is set.
|
// Default to system theme if QT_QPA_PLATFORMTHEME var is set.
|
||||||
|
@ -226,6 +247,11 @@ private:
|
||||||
QString cameraResolution_;
|
QString cameraResolution_;
|
||||||
QString cameraFrameRate_;
|
QString cameraFrameRate_;
|
||||||
bool useStunServer_;
|
bool useStunServer_;
|
||||||
|
QString profile_;
|
||||||
|
QString userId_;
|
||||||
|
QString accessToken_;
|
||||||
|
QString deviceId_;
|
||||||
|
QString homeserver_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class HorizontalLine : public QFrame
|
class HorizontalLine : public QFrame
|
||||||
|
|
64
src/main.cpp
64
src/main.cpp
|
@ -107,29 +107,7 @@ main(int argc, char *argv[])
|
||||||
// needed for settings so need to register before any settings are read to prevent warnings
|
// needed for settings so need to register before any settings are read to prevent warnings
|
||||||
qRegisterMetaType<UserSettings::Presence>();
|
qRegisterMetaType<UserSettings::Presence>();
|
||||||
|
|
||||||
// This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name
|
QCoreApplication::setApplicationName("nheko");
|
||||||
// parsed before the app name is set.
|
|
||||||
QString appName{"nheko"};
|
|
||||||
for (int i = 0; i < argc; ++i) {
|
|
||||||
if (QString{argv[i]}.startsWith("--profile=")) {
|
|
||||||
QString q{argv[i]};
|
|
||||||
q.remove("--profile=");
|
|
||||||
appName += "-" + q;
|
|
||||||
} else if (QString{argv[i]}.startsWith("--p=")) {
|
|
||||||
QString q{argv[i]};
|
|
||||||
q.remove("-p=");
|
|
||||||
appName += "-" + q;
|
|
||||||
} else if (QString{argv[i]} == "--profile" || QString{argv[i]} == "-p") {
|
|
||||||
if (i < argc - 1) // if i is less than argc - 1, we still have a parameter
|
|
||||||
// left to process as the name
|
|
||||||
{
|
|
||||||
++i; // the next arg is the name, so increment
|
|
||||||
appName += "-" + QString{argv[i]};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QCoreApplication::setApplicationName(appName);
|
|
||||||
QCoreApplication::setApplicationVersion(nheko::version);
|
QCoreApplication::setApplicationVersion(nheko::version);
|
||||||
QCoreApplication::setOrganizationName("nheko");
|
QCoreApplication::setOrganizationName("nheko");
|
||||||
QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
|
QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
|
||||||
|
@ -147,12 +125,36 @@ main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name
|
||||||
|
// parsed before the SingleApplication userdata is set.
|
||||||
|
QString userdata{""};
|
||||||
|
for (int i = 0; i < argc; ++i) {
|
||||||
|
if (QString{argv[i]}.startsWith("--profile=")) {
|
||||||
|
QString q{argv[i]};
|
||||||
|
q.remove("--profile=");
|
||||||
|
userdata = q;
|
||||||
|
} else if (QString{argv[i]}.startsWith("--p=")) {
|
||||||
|
QString q{argv[i]};
|
||||||
|
q.remove("-p=");
|
||||||
|
userdata = q;
|
||||||
|
} else if (QString{argv[i]} == "--profile" || QString{argv[i]} == "-p") {
|
||||||
|
if (i < argc - 1) // if i is less than argc - 1, we still have a parameter
|
||||||
|
// left to process as the name
|
||||||
|
{
|
||||||
|
++i; // the next arg is the name, so increment
|
||||||
|
userdata = QString{argv[i]};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SingleApplication app(argc,
|
SingleApplication app(argc,
|
||||||
argv,
|
argv,
|
||||||
false,
|
false,
|
||||||
SingleApplication::Mode::User |
|
SingleApplication::Mode::User |
|
||||||
SingleApplication::Mode::ExcludeAppPath |
|
SingleApplication::Mode::ExcludeAppPath |
|
||||||
SingleApplication::Mode::ExcludeAppVersion);
|
SingleApplication::Mode::ExcludeAppVersion,
|
||||||
|
100,
|
||||||
|
userdata);
|
||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
|
@ -194,14 +196,17 @@ main(int argc, char *argv[])
|
||||||
std::exit(1);
|
std::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSettings settings;
|
UserSettings settings;
|
||||||
|
|
||||||
|
if (parser.isSet(configName))
|
||||||
|
settings.setProfile(parser.value(configName));
|
||||||
|
|
||||||
QFont font;
|
QFont font;
|
||||||
QString userFontFamily = settings.value("user/font_family", "").toString();
|
QString userFontFamily = settings.font();
|
||||||
if (!userFontFamily.isEmpty()) {
|
if (!userFontFamily.isEmpty()) {
|
||||||
font.setFamily(userFontFamily);
|
font.setFamily(userFontFamily);
|
||||||
}
|
}
|
||||||
font.setPointSizeF(settings.value("user/font_size", font.pointSizeF()).toDouble());
|
font.setPointSizeF(settings.fontSize());
|
||||||
|
|
||||||
app.setFont(font);
|
app.setFont(font);
|
||||||
|
|
||||||
|
@ -216,13 +221,12 @@ main(int argc, char *argv[])
|
||||||
appTranslator.load(QLocale(), "nheko", "_", ":/translations");
|
appTranslator.load(QLocale(), "nheko", "_", ":/translations");
|
||||||
app.installTranslator(&appTranslator);
|
app.installTranslator(&appTranslator);
|
||||||
|
|
||||||
MainWindow w{(appName == "nheko" ? "" : appName.remove("nheko-"))};
|
MainWindow w;
|
||||||
|
|
||||||
// Move the MainWindow to the center
|
// Move the MainWindow to the center
|
||||||
w.move(screenCenter(w.width(), w.height()));
|
w.move(screenCenter(w.width(), w.height()));
|
||||||
|
|
||||||
if (!settings.value("user/window/start_in_tray", false).toBool() ||
|
if (!settings.startInTray() && !settings.tray())
|
||||||
!settings.value("user/window/tray", true).toBool())
|
|
||||||
w.show();
|
w.show();
|
||||||
|
|
||||||
QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() {
|
QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() {
|
||||||
|
|
|
@ -1,201 +0,0 @@
|
||||||
// The MIT License (MIT)
|
|
||||||
//
|
|
||||||
// Copyright (c) Itay Grudev 2015 - 2020
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in
|
|
||||||
// all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
// THE SOFTWARE.
|
|
||||||
|
|
||||||
#include <QtCore/QElapsedTimer>
|
|
||||||
#include <QtCore/QThread>
|
|
||||||
#include <QtCore/QByteArray>
|
|
||||||
#include <QtCore/QSharedMemory>
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
|
||||||
#include <QtCore/QRandomGenerator>
|
|
||||||
#else
|
|
||||||
#include <QtCore/QDateTime>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#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 {bool} allowSecondaryInstances
|
|
||||||
*/
|
|
||||||
SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout )
|
|
||||||
: app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) )
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
|
|
||||||
#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;
|
|
||||||
|
|
||||||
// Generating an application ID used for identifying the shared memory
|
|
||||||
// block and QLocalServer
|
|
||||||
d->genBlockServerName();
|
|
||||||
|
|
||||||
#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
|
|
||||||
d->memory->lock();
|
|
||||||
d->initializeMemoryBlock();
|
|
||||||
d->memory->unlock();
|
|
||||||
} else {
|
|
||||||
// Attempt to attach to the memory segment
|
|
||||||
if( ! d->memory->attach() ) {
|
|
||||||
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
|
|
||||||
qCritical() << d->memory->errorString();
|
|
||||||
delete d;
|
|
||||||
::exit( EXIT_FAILURE );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ) {
|
|
||||||
d->memory->lock();
|
|
||||||
|
|
||||||
if( d->blockChecksum() == inst->checksum ) break;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
d->memory->unlock();
|
|
||||||
|
|
||||||
// Random sleep here limits the probability of a collision between two racing apps
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
|
||||||
QThread::sleep( QRandomGenerator::global()->bounded( 8u, 18u ) );
|
|
||||||
#else
|
|
||||||
qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max() );
|
|
||||||
QThread::sleep( 8 + static_cast <unsigned long>( static_cast <float>( qrand() ) / RAND_MAX * 10 ) );
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
if( inst->primary == false) {
|
|
||||||
d->startPrimary();
|
|
||||||
d->memory->unlock();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if another instance can be started
|
|
||||||
if( allowSecondary ) {
|
|
||||||
inst->secondary += 1;
|
|
||||||
inst->checksum = d->blockChecksum();
|
|
||||||
d->instanceNumber = inst->secondary;
|
|
||||||
d->startSecondary();
|
|
||||||
if( d->options & Mode::SecondaryNotification ) {
|
|
||||||
d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance );
|
|
||||||
}
|
|
||||||
d->memory->unlock();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
d->memory->unlock();
|
|
||||||
|
|
||||||
d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance );
|
|
||||||
|
|
||||||
delete d;
|
|
||||||
|
|
||||||
::exit( EXIT_SUCCESS );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Destructor
|
|
||||||
*/
|
|
||||||
SingleApplication::~SingleApplication()
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
delete d;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SingleApplication::isPrimary()
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
return d->server != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SingleApplication::isSecondary()
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
return d->server == nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
quint32 SingleApplication::instanceId()
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
return d->instanceNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
qint64 SingleApplication::primaryPid()
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
return d->primaryPid();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SingleApplication::primaryUser()
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
return d->primaryUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SingleApplication::currentUser()
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
return d->getUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SingleApplication::sendMessage( const QByteArray &message, int timeout )
|
|
||||||
{
|
|
||||||
Q_D(SingleApplication);
|
|
||||||
|
|
||||||
// Nobody to connect to
|
|
||||||
if( isPrimary() ) return false;
|
|
||||||
|
|
||||||
// Make sure the socket is connected
|
|
||||||
d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect );
|
|
||||||
|
|
||||||
d->socket->write( message );
|
|
||||||
bool dataWritten = d->socket->waitForBytesWritten( timeout );
|
|
||||||
d->socket->flush();
|
|
||||||
return dataWritten;
|
|
||||||
}
|
|
|
@ -3,6 +3,30 @@ Changelog
|
||||||
|
|
||||||
If by accident I have forgotten to credit someone in the CHANGELOG, email me and I will fix it.
|
If by accident I have forgotten to credit someone in the CHANGELOG, email me and I will fix it.
|
||||||
|
|
||||||
|
__3.2.0__
|
||||||
|
---------
|
||||||
|
|
||||||
|
* Added support for Qt 6 - _Jonas Kvinge_
|
||||||
|
* Fixed warning in `Qt 5.9` with `min`/`max` functions on Windows - _Nick Korotysh_
|
||||||
|
* Fix return value of connectToPrimary() when connect is successful - _Jonas Kvinge_
|
||||||
|
* Fix build issue with MinGW GCC pedantic mode - _Iakov Kirilenko_
|
||||||
|
* Fixed conversion from `int` to `quint32` and Clang Tidy warnings - _Hennadii Chernyshchyk_
|
||||||
|
|
||||||
|
__3.1.5__
|
||||||
|
---------
|
||||||
|
|
||||||
|
* Improved library stability in edge cases and very rapid process initialisation
|
||||||
|
* Fixed Bug where the shared memory block may have been modified without a lock
|
||||||
|
* Fixed Bug causing `instanceStarted()` to not get emitted when a second instance
|
||||||
|
has been started before the primary has initiated it's `QLocalServer`.
|
||||||
|
|
||||||
|
__3.1.4__
|
||||||
|
---------
|
||||||
|
* Officially supporting and build-testing against Qt 5.15
|
||||||
|
* Fixed an MSVC C4996 warning that suggests using `strncpy_s`.
|
||||||
|
|
||||||
|
_Hennadii Chernyshchyk_
|
||||||
|
|
||||||
__3.1.3.1__
|
__3.1.3.1__
|
||||||
---------
|
---------
|
||||||
* CMake build system improvements
|
* CMake build system improvements
|
||||||
|
@ -38,18 +62,18 @@ __3.1.0a__
|
||||||
__3.0.19__
|
__3.0.19__
|
||||||
----------
|
----------
|
||||||
|
|
||||||
* Fixed code warning for depricated functions in Qt 5.10 related to `QTime` and `qrand()`.
|
* Fixed code warning for depricated functions in Qt 5.10 related to `QTime` and `qrand()`.
|
||||||
|
|
||||||
_Hennadii Chernyshchyk_
|
_Hennadii Chernyshchyk_
|
||||||
_Anton Filimonov_
|
_Anton Filimonov_
|
||||||
_Jonas Kvinge_
|
_Jonas Kvinge_
|
||||||
|
|
||||||
__3.0.18__
|
__3.0.18__
|
||||||
----------
|
----------
|
||||||
|
|
||||||
* Fallback to standard QApplication class on iOS and Android systems where
|
* Fallback to standard QApplication class on iOS and Android systems where
|
||||||
the library is not supported.
|
the library is not supported.
|
||||||
|
|
||||||
* Added Build CI tests to verify the library builds successfully on Linux, Windows and MacOS across multiple Qt versions.
|
* Added Build CI tests to verify the library builds successfully on Linux, Windows and MacOS across multiple Qt versions.
|
||||||
|
|
||||||
_Anton Filimonov_
|
_Anton Filimonov_
|
|
@ -10,22 +10,28 @@ add_library(${PROJECT_NAME} STATIC
|
||||||
)
|
)
|
||||||
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
|
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
|
||||||
|
|
||||||
|
if(NOT QT_DEFAULT_MAJOR_VERSION)
|
||||||
|
set(QT_DEFAULT_MAJOR_VERSION 5 CACHE STRING "Qt version to use (5 or 6), defaults to 5")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Find dependencies
|
# Find dependencies
|
||||||
find_package(Qt5 COMPONENTS Network REQUIRED)
|
set(QT_COMPONENTS Core Network)
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Network)
|
set(QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Core Qt${QT_DEFAULT_MAJOR_VERSION}::Network)
|
||||||
|
|
||||||
if(QAPPLICATION_CLASS STREQUAL QApplication)
|
if(QAPPLICATION_CLASS STREQUAL QApplication)
|
||||||
find_package(Qt5 COMPONENTS Widgets REQUIRED)
|
list(APPEND QT_COMPONENTS Widgets)
|
||||||
target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Widgets)
|
list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Widgets)
|
||||||
elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication)
|
elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication)
|
||||||
find_package(Qt5 COMPONENTS Gui REQUIRED)
|
list(APPEND QT_COMPONENTS Gui)
|
||||||
target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Gui)
|
list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Gui)
|
||||||
else()
|
else()
|
||||||
set(QAPPLICATION_CLASS QCoreApplication)
|
set(QAPPLICATION_CLASS QCoreApplication)
|
||||||
find_package(Qt5 COMPONENTS Core REQUIRED)
|
|
||||||
target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Core)
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED)
|
||||||
|
|
||||||
|
target_link_libraries(${PROJECT_NAME} PUBLIC ${QT_LIBRARIES})
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE advapi32)
|
target_link_libraries(${PROJECT_NAME} PRIVATE advapi32)
|
||||||
endif()
|
endif()
|
|
@ -2,7 +2,7 @@ SingleApplication
|
||||||
=================
|
=================
|
||||||
[![CI](https://github.com/itay-grudev/SingleApplication/workflows/CI:%20Build%20Test/badge.svg)](https://github.com/itay-grudev/SingleApplication/actions)
|
[![CI](https://github.com/itay-grudev/SingleApplication/workflows/CI:%20Build%20Test/badge.svg)](https://github.com/itay-grudev/SingleApplication/actions)
|
||||||
|
|
||||||
This is a replacement of the QtSingleApplication for `Qt5`.
|
This is a replacement of the QtSingleApplication for `Qt5` and `Qt6`.
|
||||||
|
|
||||||
Keeps the Primary Instance of your Application and kills each subsequent
|
Keeps the Primary Instance of your Application and kills each subsequent
|
||||||
instances. It can (if enabled) spawn secondary (non-related to the primary)
|
instances. It can (if enabled) spawn secondary (non-related to the primary)
|
||||||
|
@ -139,13 +139,22 @@ app.isSecondary();
|
||||||
*__Note:__ If your Primary Instance is terminated a newly launched instance
|
*__Note:__ If your Primary Instance is terminated a newly launched instance
|
||||||
will replace the Primary one even if the Secondary flag has been set.*
|
will replace the Primary one even if the Secondary flag has been set.*
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
There are three examples provided in this repository:
|
||||||
|
|
||||||
|
* Basic example that prevents a secondary instance from starting [`examples/basic`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/basic)
|
||||||
|
* An example of a graphical application raising it's parent window [`examples/calculator`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/calculator)
|
||||||
|
* A console application sending the primary instance it's command line parameters [`examples/sending_arguments`](https://github.com/itay-grudev/SingleApplication/tree/master/examples/sending_arguments)
|
||||||
|
|
||||||
API
|
API
|
||||||
---
|
---
|
||||||
|
|
||||||
### Members
|
### Members
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 100 )
|
SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 100, QString userData = QString() )
|
||||||
```
|
```
|
||||||
|
|
||||||
Depending on whether `allowSecondary` is set, this constructor may terminate
|
Depending on whether `allowSecondary` is set, this constructor may terminate
|
||||||
|
@ -154,7 +163,7 @@ can be specified to set whether the SingleApplication block should work
|
||||||
user-wide or system-wide. Additionally the `Mode::SecondaryNotification` may be
|
user-wide or system-wide. Additionally the `Mode::SecondaryNotification` may be
|
||||||
used to notify the primary instance whenever a secondary instance had been
|
used to notify the primary instance whenever a secondary instance had been
|
||||||
started (disabled by default). `timeout` specifies the maximum time in
|
started (disabled by default). `timeout` specifies the maximum time in
|
||||||
milliseconds to wait for blocking operations.
|
milliseconds to wait for blocking operations. Setting `userData` provides additional data that will isolate this instance from other instances that do not have the same (or any) user data set.
|
||||||
|
|
||||||
*__Note:__ `argc` and `argv` may be changed as Qt removes arguments that it
|
*__Note:__ `argc` and `argv` may be changed as Qt removes arguments that it
|
||||||
recognizes.*
|
recognizes.*
|
274
third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp
vendored
Normal file
274
third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp
vendored
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
// The MIT License (MIT)
|
||||||
|
//
|
||||||
|
// Copyright (c) Itay Grudev 2015 - 2020
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
#include <QtCore/QElapsedTimer>
|
||||||
|
#include <QtCore/QByteArray>
|
||||||
|
#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, 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()
|
||||||
|
{
|
||||||
|
Q_D( 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()
|
||||||
|
{
|
||||||
|
Q_D( 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()
|
||||||
|
{
|
||||||
|
Q_D( 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()
|
||||||
|
{
|
||||||
|
Q_D( 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()
|
||||||
|
{
|
||||||
|
Q_D( 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()
|
||||||
|
{
|
||||||
|
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.
|
||||||
|
* @return true if the message was sent successfuly, false otherwise.
|
||||||
|
*/
|
||||||
|
bool SingleApplication::sendMessage( const QByteArray &message, int timeout )
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
d->socket->write( message );
|
||||||
|
bool dataWritten = d->socket->waitForBytesWritten( timeout );
|
||||||
|
d->socket->flush();
|
||||||
|
return dataWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
{
|
||||||
|
Q_D( SingleApplication );
|
||||||
|
return d->appData();
|
||||||
|
}
|
|
@ -85,7 +85,7 @@ public:
|
||||||
* Usually 4*timeout would be the worst case (fail) scenario.
|
* Usually 4*timeout would be the worst case (fail) scenario.
|
||||||
* @see See the corresponding QAPPLICATION_CLASS constructor for reference
|
* @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 );
|
explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, QString userData = QString() );
|
||||||
~SingleApplication() override;
|
~SingleApplication() override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,6 +133,12 @@ public:
|
||||||
*/
|
*/
|
||||||
bool sendMessage( const QByteArray &message, int timeout = 100 );
|
bool sendMessage( const QByteArray &message, int timeout = 100 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the set user data.
|
||||||
|
* @returns {QStringList}
|
||||||
|
*/
|
||||||
|
QStringList userData();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void instanceStarted();
|
void instanceStarted();
|
||||||
void receivedMessage( quint32 instanceId, QByteArray message );
|
void receivedMessage( quint32 instanceId, QByteArray message );
|
||||||
|
@ -140,6 +146,7 @@ Q_SIGNALS:
|
||||||
private:
|
private:
|
||||||
SingleApplicationPrivate *d_ptr;
|
SingleApplicationPrivate *d_ptr;
|
||||||
Q_DECLARE_PRIVATE(SingleApplication)
|
Q_DECLARE_PRIVATE(SingleApplication)
|
||||||
|
void abortSafely();
|
||||||
};
|
};
|
||||||
|
|
||||||
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options)
|
Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options)
|
|
@ -33,12 +33,20 @@
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
#include <QtCore/QDir>
|
#include <QtCore/QDir>
|
||||||
|
#include <QtCore/QThread>
|
||||||
#include <QtCore/QByteArray>
|
#include <QtCore/QByteArray>
|
||||||
#include <QtCore/QDataStream>
|
#include <QtCore/QDataStream>
|
||||||
|
#include <QtCore/QElapsedTimer>
|
||||||
#include <QtCore/QCryptographicHash>
|
#include <QtCore/QCryptographicHash>
|
||||||
#include <QtNetwork/QLocalServer>
|
#include <QtNetwork/QLocalServer>
|
||||||
#include <QtNetwork/QLocalSocket>
|
#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.h"
|
||||||
#include "singleapplication_p.h"
|
#include "singleapplication_p.h"
|
||||||
|
|
||||||
|
@ -49,6 +57,9 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX 1
|
||||||
|
#endif
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <lmcons.h>
|
#include <lmcons.h>
|
||||||
#endif
|
#endif
|
||||||
|
@ -59,20 +70,20 @@ SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr )
|
||||||
server = nullptr;
|
server = nullptr;
|
||||||
socket = nullptr;
|
socket = nullptr;
|
||||||
memory = nullptr;
|
memory = nullptr;
|
||||||
instanceNumber = -1;
|
instanceNumber = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
SingleApplicationPrivate::~SingleApplicationPrivate()
|
SingleApplicationPrivate::~SingleApplicationPrivate()
|
||||||
{
|
{
|
||||||
if( socket != nullptr ) {
|
if( socket != nullptr ){
|
||||||
socket->close();
|
socket->close();
|
||||||
delete socket;
|
delete socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( memory != nullptr ) {
|
if( memory != nullptr ){
|
||||||
memory->lock();
|
memory->lock();
|
||||||
auto *inst = static_cast<InstancesInfo*>(memory->data());
|
auto *inst = static_cast<InstancesInfo*>(memory->data());
|
||||||
if( server != nullptr ) {
|
if( server != nullptr ){
|
||||||
server->close();
|
server->close();
|
||||||
delete server;
|
delete server;
|
||||||
inst->primary = false;
|
inst->primary = false;
|
||||||
|
@ -106,7 +117,7 @@ QString SingleApplicationPrivate::getUsername()
|
||||||
struct passwd *pw = getpwuid( uid );
|
struct passwd *pw = getpwuid( uid );
|
||||||
if( pw )
|
if( pw )
|
||||||
username = QString::fromLocal8Bit( pw->pw_name );
|
username = QString::fromLocal8Bit( pw->pw_name );
|
||||||
if ( username.isEmpty() ) {
|
if ( username.isEmpty() ){
|
||||||
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
||||||
username = QString::fromLocal8Bit( qgetenv( "USER" ) );
|
username = QString::fromLocal8Bit( qgetenv( "USER" ) );
|
||||||
#else
|
#else
|
||||||
|
@ -125,11 +136,14 @@ void SingleApplicationPrivate::genBlockServerName()
|
||||||
appData.addData( SingleApplication::app_t::organizationName().toUtf8() );
|
appData.addData( SingleApplication::app_t::organizationName().toUtf8() );
|
||||||
appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() );
|
appData.addData( SingleApplication::app_t::organizationDomain().toUtf8() );
|
||||||
|
|
||||||
if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ) {
|
if ( ! appDataList.isEmpty() )
|
||||||
|
appData.addData( appDataList.join( "" ).toUtf8() );
|
||||||
|
|
||||||
|
if( ! (options & SingleApplication::Mode::ExcludeAppVersion) ){
|
||||||
appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() );
|
appData.addData( SingleApplication::app_t::applicationVersion().toUtf8() );
|
||||||
}
|
}
|
||||||
|
|
||||||
if( ! (options & SingleApplication::Mode::ExcludeAppPath) ) {
|
if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() );
|
appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() );
|
||||||
#else
|
#else
|
||||||
|
@ -138,7 +152,7 @@ void SingleApplicationPrivate::genBlockServerName()
|
||||||
}
|
}
|
||||||
|
|
||||||
// User level block requires a user specific data in the hash
|
// User level block requires a user specific data in the hash
|
||||||
if( options & SingleApplication::Mode::User ) {
|
if( options & SingleApplication::Mode::User ){
|
||||||
appData.addData( getUsername().toUtf8() );
|
appData.addData( getUsername().toUtf8() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +161,7 @@ void SingleApplicationPrivate::genBlockServerName()
|
||||||
blockServerName = appData.result().toBase64().replace("/", "_");
|
blockServerName = appData.result().toBase64().replace("/", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
void SingleApplicationPrivate::initializeMemoryBlock()
|
void SingleApplicationPrivate::initializeMemoryBlock() const
|
||||||
{
|
{
|
||||||
auto *inst = static_cast<InstancesInfo*>( memory->data() );
|
auto *inst = static_cast<InstancesInfo*>( memory->data() );
|
||||||
inst->primary = false;
|
inst->primary = false;
|
||||||
|
@ -159,8 +173,14 @@ void SingleApplicationPrivate::initializeMemoryBlock()
|
||||||
|
|
||||||
void SingleApplicationPrivate::startPrimary()
|
void SingleApplicationPrivate::startPrimary()
|
||||||
{
|
{
|
||||||
Q_Q(SingleApplication);
|
// 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
|
// Successful creation means that no main process exists
|
||||||
// So we start a QLocalServer to listen for connections
|
// So we start a QLocalServer to listen for connections
|
||||||
QLocalServer::removeServer( blockServerName );
|
QLocalServer::removeServer( blockServerName );
|
||||||
|
@ -168,10 +188,10 @@ void SingleApplicationPrivate::startPrimary()
|
||||||
|
|
||||||
// Restrict access to the socket according to the
|
// Restrict access to the socket according to the
|
||||||
// SingleApplication::Mode::User flag on User level or no restrictions
|
// SingleApplication::Mode::User flag on User level or no restrictions
|
||||||
if( options & SingleApplication::Mode::User ) {
|
if( options & SingleApplication::Mode::User ){
|
||||||
server->setSocketOptions( QLocalServer::UserAccessOption );
|
server->setSocketOptions( QLocalServer::UserAccessOption );
|
||||||
} else {
|
} else {
|
||||||
server->setSocketOptions( QLocalServer::WorldAccessOption );
|
server->setSocketOptions( QLocalServer::WorldAccessOption );
|
||||||
}
|
}
|
||||||
|
|
||||||
server->listen( blockServerName );
|
server->listen( blockServerName );
|
||||||
|
@ -181,87 +201,95 @@ void SingleApplicationPrivate::startPrimary()
|
||||||
this,
|
this,
|
||||||
&SingleApplicationPrivate::slotConnectionEstablished
|
&SingleApplicationPrivate::slotConnectionEstablished
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset the number of connections
|
|
||||||
auto *inst = static_cast <InstancesInfo*>( memory->data() );
|
|
||||||
|
|
||||||
inst->primary = true;
|
|
||||||
inst->primaryPid = q->applicationPid();
|
|
||||||
strncpy( inst->primaryUser, getUsername().toUtf8().data(), 127 );
|
|
||||||
inst->primaryUser[127] = '\0';
|
|
||||||
inst->checksum = blockChecksum();
|
|
||||||
|
|
||||||
instanceNumber = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SingleApplicationPrivate::startSecondary()
|
void SingleApplicationPrivate::startSecondary()
|
||||||
{
|
{
|
||||||
|
auto *inst = static_cast <InstancesInfo*>( memory->data() );
|
||||||
|
|
||||||
|
inst->secondary += 1;
|
||||||
|
inst->checksum = blockChecksum();
|
||||||
|
instanceNumber = inst->secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType )
|
bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType connectionType )
|
||||||
{
|
{
|
||||||
|
QElapsedTimer time;
|
||||||
|
time.start();
|
||||||
|
|
||||||
// Connect to the Local Server of the Primary Instance if not already
|
// Connect to the Local Server of the Primary Instance if not already
|
||||||
// connected.
|
// connected.
|
||||||
if( socket == nullptr ) {
|
if( socket == nullptr ){
|
||||||
socket = new QLocalSocket();
|
socket = new QLocalSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already connected - we are done;
|
if( socket->state() == QLocalSocket::ConnectedState ) return true;
|
||||||
if( socket->state() == QLocalSocket::ConnectedState )
|
|
||||||
return;
|
|
||||||
|
|
||||||
// If not connect
|
if( socket->state() != QLocalSocket::ConnectedState ){
|
||||||
if( socket->state() == QLocalSocket::UnconnectedState ||
|
|
||||||
socket->state() == QLocalSocket::ClosingState ) {
|
|
||||||
socket->connectToServer( blockServerName );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for being connected
|
while( true ){
|
||||||
if( socket->state() == QLocalSocket::ConnectingState ) {
|
randomSleep();
|
||||||
socket->waitForConnected( msecs );
|
|
||||||
|
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
|
// Initialisation message according to the SingleApplication protocol
|
||||||
if( socket->state() == QLocalSocket::ConnectedState ) {
|
QByteArray initMsg;
|
||||||
// Notify the parent that a new instance had been started;
|
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
|
||||||
QByteArray initMsg;
|
|
||||||
QDataStream writeStream(&initMsg, QIODevice::WriteOnly);
|
|
||||||
|
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
||||||
writeStream.setVersion(QDataStream::Qt_5_6);
|
writeStream.setVersion(QDataStream::Qt_5_6);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
writeStream << blockServerName.toLatin1();
|
writeStream << blockServerName.toLatin1();
|
||||||
writeStream << static_cast<quint8>(connectionType);
|
writeStream << static_cast<quint8>(connectionType);
|
||||||
writeStream << instanceNumber;
|
writeStream << instanceNumber;
|
||||||
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
writeStream << checksum;
|
quint16 checksum = qChecksum(QByteArray(initMsg, static_cast<quint32>(initMsg.length())));
|
||||||
|
#else
|
||||||
|
quint16 checksum = qChecksum(initMsg.constData(), static_cast<quint32>(initMsg.length()));
|
||||||
|
#endif
|
||||||
|
writeStream << checksum;
|
||||||
|
|
||||||
// The header indicates the message length that follows
|
// The header indicates the message length that follows
|
||||||
QByteArray header;
|
QByteArray header;
|
||||||
QDataStream headerStream(&header, QIODevice::WriteOnly);
|
QDataStream headerStream(&header, QIODevice::WriteOnly);
|
||||||
|
|
||||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
|
||||||
headerStream.setVersion(QDataStream::Qt_5_6);
|
headerStream.setVersion(QDataStream::Qt_5_6);
|
||||||
#endif
|
#endif
|
||||||
headerStream << static_cast <quint64>( initMsg.length() );
|
headerStream << static_cast <quint64>( initMsg.length() );
|
||||||
|
|
||||||
socket->write( header );
|
socket->write( header );
|
||||||
socket->write( initMsg );
|
socket->write( initMsg );
|
||||||
socket->flush();
|
bool result = socket->waitForBytesWritten( static_cast<int>(msecs - time.elapsed()) );
|
||||||
socket->waitForBytesWritten( msecs );
|
socket->flush();
|
||||||
}
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
quint16 SingleApplicationPrivate::blockChecksum()
|
quint16 SingleApplicationPrivate::blockChecksum() const
|
||||||
{
|
{
|
||||||
return qChecksum(
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
static_cast <const char *>( memory->data() ),
|
quint16 checksum = qChecksum(QByteArray(static_cast<const char*>(memory->constData()), offsetof(InstancesInfo, checksum)));
|
||||||
offsetof( InstancesInfo, checksum )
|
#else
|
||||||
);
|
quint16 checksum = qChecksum(static_cast<const char*>(memory->constData()), offsetof(InstancesInfo, checksum));
|
||||||
|
#endif
|
||||||
|
return checksum;
|
||||||
}
|
}
|
||||||
|
|
||||||
qint64 SingleApplicationPrivate::primaryPid()
|
qint64 SingleApplicationPrivate::primaryPid() const
|
||||||
{
|
{
|
||||||
qint64 pid;
|
qint64 pid;
|
||||||
|
|
||||||
|
@ -273,7 +301,7 @@ qint64 SingleApplicationPrivate::primaryPid()
|
||||||
return pid;
|
return pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString SingleApplicationPrivate::primaryUser()
|
QString SingleApplicationPrivate::primaryUser() const
|
||||||
{
|
{
|
||||||
QByteArray username;
|
QByteArray username;
|
||||||
|
|
||||||
|
@ -294,7 +322,7 @@ void SingleApplicationPrivate::slotConnectionEstablished()
|
||||||
connectionMap.insert(nextConnSocket, ConnectionInfo());
|
connectionMap.insert(nextConnSocket, ConnectionInfo());
|
||||||
|
|
||||||
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose,
|
QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose,
|
||||||
[nextConnSocket, this]() {
|
[nextConnSocket, this](){
|
||||||
auto &info = connectionMap[nextConnSocket];
|
auto &info = connectionMap[nextConnSocket];
|
||||||
Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId );
|
Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId );
|
||||||
}
|
}
|
||||||
|
@ -308,9 +336,9 @@ void SingleApplicationPrivate::slotConnectionEstablished()
|
||||||
);
|
);
|
||||||
|
|
||||||
QObject::connect(nextConnSocket, &QLocalSocket::readyRead,
|
QObject::connect(nextConnSocket, &QLocalSocket::readyRead,
|
||||||
[nextConnSocket, this]() {
|
[nextConnSocket, this](){
|
||||||
auto &info = connectionMap[nextConnSocket];
|
auto &info = connectionMap[nextConnSocket];
|
||||||
switch(info.stage) {
|
switch(info.stage){
|
||||||
case StageHeader:
|
case StageHeader:
|
||||||
readInitMessageHeader(nextConnSocket);
|
readInitMessageHeader(nextConnSocket);
|
||||||
break;
|
break;
|
||||||
|
@ -329,11 +357,11 @@ void SingleApplicationPrivate::slotConnectionEstablished()
|
||||||
|
|
||||||
void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock )
|
void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock )
|
||||||
{
|
{
|
||||||
if (!connectionMap.contains( sock )) {
|
if (!connectionMap.contains( sock )){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ) {
|
if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,7 +378,7 @@ void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock )
|
||||||
info.stage = StageBody;
|
info.stage = StageBody;
|
||||||
info.msgLen = msgLen;
|
info.msgLen = msgLen;
|
||||||
|
|
||||||
if ( sock->bytesAvailable() >= (qint64) msgLen ) {
|
if ( sock->bytesAvailable() >= (qint64) msgLen ){
|
||||||
readInitMessageBody( sock );
|
readInitMessageBody( sock );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -359,12 +387,12 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
|
||||||
{
|
{
|
||||||
Q_Q(SingleApplication);
|
Q_Q(SingleApplication);
|
||||||
|
|
||||||
if (!connectionMap.contains( sock )) {
|
if (!connectionMap.contains( sock )){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionInfo &info = connectionMap[sock];
|
ConnectionInfo &info = connectionMap[sock];
|
||||||
if( sock->bytesAvailable() < ( qint64 )info.msgLen ) {
|
if( sock->bytesAvailable() < ( qint64 )info.msgLen ){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,13 +422,17 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
|
||||||
quint16 msgChecksum = 0;
|
quint16 msgChecksum = 0;
|
||||||
readStream >> msgChecksum;
|
readStream >> msgChecksum;
|
||||||
|
|
||||||
const quint16 actualChecksum = qChecksum( msgBytes.constData(), static_cast<quint32>( msgBytes.length() - sizeof( quint16 ) ) );
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, 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 &&
|
bool isValid = readStream.status() == QDataStream::Ok &&
|
||||||
QLatin1String(latin1Name) == blockServerName &&
|
QLatin1String(latin1Name) == blockServerName &&
|
||||||
msgChecksum == actualChecksum;
|
msgChecksum == actualChecksum;
|
||||||
|
|
||||||
if( !isValid ) {
|
if( !isValid ){
|
||||||
sock->close();
|
sock->close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -415,7 +447,7 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
|
||||||
Q_EMIT q->instanceStarted();
|
Q_EMIT q->instanceStarted();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sock->bytesAvailable() > 0) {
|
if (sock->bytesAvailable() > 0){
|
||||||
Q_EMIT this->slotDataAvailable( sock, instanceId );
|
Q_EMIT this->slotDataAvailable( sock, instanceId );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -431,3 +463,23 @@ void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedS
|
||||||
if( closedSocket->bytesAvailable() > 0 )
|
if( closedSocket->bytesAvailable() > 0 )
|
||||||
Q_EMIT slotDataAvailable( closedSocket, instanceId );
|
Q_EMIT 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( 8 + static_cast <unsigned long>( static_cast <float>( qrand() ) / RAND_MAX * 10 ));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void SingleApplicationPrivate::addAppData(const QString &data)
|
||||||
|
{
|
||||||
|
appDataList.push_back(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList SingleApplicationPrivate::appData() const
|
||||||
|
{
|
||||||
|
return appDataList;
|
||||||
|
}
|
|
@ -41,8 +41,8 @@ struct InstancesInfo {
|
||||||
bool primary;
|
bool primary;
|
||||||
quint32 secondary;
|
quint32 secondary;
|
||||||
qint64 primaryPid;
|
qint64 primaryPid;
|
||||||
quint16 checksum;
|
|
||||||
char primaryUser[128];
|
char primaryUser[128];
|
||||||
|
quint16 checksum; // Must be the last field
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ConnectionInfo {
|
struct ConnectionInfo {
|
||||||
|
@ -70,17 +70,20 @@ public:
|
||||||
SingleApplicationPrivate( SingleApplication *q_ptr );
|
SingleApplicationPrivate( SingleApplication *q_ptr );
|
||||||
~SingleApplicationPrivate() override;
|
~SingleApplicationPrivate() override;
|
||||||
|
|
||||||
QString getUsername();
|
static QString getUsername();
|
||||||
void genBlockServerName();
|
void genBlockServerName();
|
||||||
void initializeMemoryBlock();
|
void initializeMemoryBlock() const;
|
||||||
void startPrimary();
|
void startPrimary();
|
||||||
void startSecondary();
|
void startSecondary();
|
||||||
void connectToPrimary(int msecs, ConnectionType connectionType );
|
bool connectToPrimary( int msecs, ConnectionType connectionType );
|
||||||
quint16 blockChecksum();
|
quint16 blockChecksum() const;
|
||||||
qint64 primaryPid();
|
qint64 primaryPid() const;
|
||||||
QString primaryUser();
|
QString primaryUser() const;
|
||||||
void readInitMessageHeader(QLocalSocket *socket);
|
void readInitMessageHeader(QLocalSocket *socket);
|
||||||
void readInitMessageBody(QLocalSocket *socket);
|
void readInitMessageBody(QLocalSocket *socket);
|
||||||
|
static void randomSleep();
|
||||||
|
void addAppData(const QString &data);
|
||||||
|
QStringList appData() const;
|
||||||
|
|
||||||
SingleApplication *q_ptr;
|
SingleApplication *q_ptr;
|
||||||
QSharedMemory *memory;
|
QSharedMemory *memory;
|
||||||
|
@ -90,6 +93,7 @@ public:
|
||||||
QString blockServerName;
|
QString blockServerName;
|
||||||
SingleApplication::Options options;
|
SingleApplication::Options options;
|
||||||
QMap<QLocalSocket*, ConnectionInfo> connectionMap;
|
QMap<QLocalSocket*, ConnectionInfo> connectionMap;
|
||||||
|
QStringList appDataList;
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
void slotConnectionEstablished();
|
void slotConnectionEstablished();
|
Loading…
Reference in a new issue