Switch profile code to a more flexible method

This introduces a new version of SingleApplication as well.
This commit is contained in:
LorenDB 2020-11-09 21:28:41 -05:00 committed by Loren Burkholder
parent 100b5e0371
commit 53f45bdb1c
21 changed files with 654 additions and 373 deletions

View file

@ -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)

View file

@ -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

View file

@ -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();

View file

@ -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

View file

@ -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_;
}; };

View file

@ -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();
} }

View file

@ -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

View file

@ -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]() {

View file

@ -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;
}

View file

@ -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

View file

@ -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()

View file

@ -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.*

View 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();
}

View file

@ -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)

View file

@ -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;
}

View file

@ -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();