diff --git a/CMakeLists.txt b/CMakeLists.txt index 326e5794..8d44386e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -449,7 +449,7 @@ pkg_check_modules(GSTREAMER IMPORTED_TARGET gstreamer-sdp-1.0>=1.14 gstreamer-we # single instance functionality 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) diff --git a/README.md b/README.md index 3e4cfa55..f24b8d13 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Specifically there is support for: - Basic communities support. - Room switcher (ctrl-K). - Light, Dark & System themes. -- Creating separate profiles (command line only, use `--profile=name`). +- Creating separate profiles (command line only, use `-p name`). ## Installation diff --git a/src/Cache.cpp b/src/Cache.cpp index 674b5793..986de221 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include "Logging.h" #include "MatrixClient.h" #include "Olm.h" +#include "UserSettingsPage.h" #include "Utils.h" //! Should be changed when a breaking change occurs in the cache format. @@ -165,17 +167,15 @@ Cache::Cache(const QString &userId, QObject *parent) void Cache::setup() { - nhlog::db()->debug("setting up cache"); + UserSettings settings; - auto statePath = QString("%1/%2") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString::fromUtf8(localUserId_.toUtf8().toHex())); + nhlog::db()->debug("setting up cache"); - cacheDirectory_ = QString("%1/%2") + cacheDirectory_ = QString("%1/%2%3") .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_.set_mapsize(DB_SIZE); @@ -184,9 +184,9 @@ Cache::setup() if (isInitial) { nhlog::db()->info("initializing LMDB"); - if (!QDir().mkpath(statePath)) { + if (!QDir().mkpath(cacheDirectory_)) { 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 // it can really mess up our database, so we shouldn't. For now, hopefully // 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) { if (e.code() != MDB_VERSION_MISMATCH && e.code() != MDB_INVALID) { 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()); - QDir stateDir(statePath); + QDir stateDir(cacheDirectory_); for (const auto &file : stateDir.entryList(QDir::NoDotAndDotDot)) { if (!stateDir.remove(file)) throw std::runtime_error( ("Unable to delete file " + file).toStdString().c_str()); } - - env_.open(statePath.toStdString().c_str()); + env_.open(cacheDirectory_.toStdString().c_str()); } auto txn = lmdb::txn::begin(env_); @@ -577,10 +576,14 @@ Cache::restoreOlmAccount() void Cache::storeSecret(const std::string &name, const std::string &secret) { + UserSettings settings; QKeychain::WritePasswordJob job(QCoreApplication::applicationName()); job.setAutoDelete(false); 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)); QEventLoop loop; job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); @@ -598,10 +601,14 @@ Cache::storeSecret(const std::string &name, const std::string &secret) void Cache::deleteSecret(const std::string &name) { + UserSettings settings; QKeychain::DeletePasswordJob job(QCoreApplication::applicationName()); job.setAutoDelete(false); job.setInsecureFallback(true); - job.setKey(QString::fromStdString(name)); + job.setKey( + "matrix." + + QString(QCryptographicHash::hash(settings.profile().toUtf8(), QCryptographicHash::Sha256)) + + "." + name.c_str()); QEventLoop loop; job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); job.start(); @@ -613,10 +620,14 @@ Cache::deleteSecret(const std::string &name) std::optional Cache::secret(const std::string &name) { + UserSettings settings; QKeychain::ReadPasswordJob job(QCoreApplication::applicationName()); job.setAutoDelete(false); job.setInsecureFallback(true); - job.setKey(QString::fromStdString(name)); + job.setKey( + "matrix." + + QString(QCryptographicHash::hash(settings.profile().toUtf8(), QCryptographicHash::Sha256)) + + "." + name.c_str()); QEventLoop loop; job.connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); job.start(); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index d056aca6..ffd0e30b 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -55,9 +55,9 @@ MainWindow *MainWindow::instance_ = nullptr; -MainWindow::MainWindow(const QString profile, QWidget *parent) - : QMainWindow(parent) - , profile_{profile} +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent), + userSettings_{QSharedPointer{new UserSettings}} { setWindowTitle(0); setObjectName("MainWindow"); @@ -70,8 +70,7 @@ MainWindow::MainWindow(const QString profile, QWidget *parent) font.setStyleStrategy(QFont::PreferAntialias); setFont(font); - userSettings_ = QSharedPointer(new UserSettings); - trayIcon_ = new TrayIcon(":/logos/nheko.svg", this); + trayIcon_ = new TrayIcon(":/logos/nheko.svg", this); welcome_page_ = new WelcomePage(this); login_page_ = new LoginPage(this); @@ -150,15 +149,13 @@ MainWindow::MainWindow(const QString profile, QWidget *parent) chat_page_->showQuickSwitcher(); }); - QSettings settings; - trayIcon_->setVisible(userSettings_->tray()); if (hasActiveUser()) { - QString token = settings.value("auth/access_token").toString(); - QString home_server = settings.value("auth/home_server").toString(); - QString user_id = settings.value("auth/user_id").toString(); - QString device_id = settings.value("auth/device_id").toString(); + QString token = userSettings_->accessToken(); + QString home_server = userSettings_->homeserver(); + QString user_id = userSettings_->userId(); + QString device_id = userSettings_->deviceId(); http::client()->set_access_token(token.toStdString()); http::client()->set_server(home_server.toStdString()); @@ -184,8 +181,9 @@ void MainWindow::setWindowTitle(int notificationCount) { QString name = "nheko"; - if (!profile_.isEmpty()) - name += " | " + profile_; + + if (!userSettings_.data()->profile().isEmpty()) + name += " | " + userSettings_.data()->profile(); if (notificationCount > 0) { name.append(QString{" (%1)"}.arg(notificationCount)); } @@ -279,11 +277,10 @@ MainWindow::showChatPage() std::to_string(http::client()->port())); auto token = QString::fromStdString(http::client()->access_token()); - QSettings settings; - settings.setValue("auth/access_token", token); - settings.setValue("auth/home_server", homeserver); - settings.setValue("auth/user_id", userid); - settings.setValue("auth/device_id", device_id); + userSettings_.data()->setUserId(userid); + userSettings_.data()->setAccessToken(token); + userSettings_.data()->setDeviceId(device_id); + userSettings_.data()->setHomeserver(homeserver); showOverlayProgressBar(); @@ -341,9 +338,13 @@ bool MainWindow::hasActiveUser() { QSettings settings; + QString prefix; + if (userSettings_->profile() != "") + prefix = "profile/" + userSettings_->profile() + "/"; - return settings.contains("auth/access_token") && settings.contains("auth/home_server") && - settings.contains("auth/user_id"); + return settings.contains(prefix + "auth/access_token") && + settings.contains(prefix + "auth/home_server") && + settings.contains(prefix + "auth/user_id"); } void diff --git a/src/MainWindow.h b/src/MainWindow.h index 2f9ff897..0915a849 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -62,7 +62,7 @@ class MainWindow : public QMainWindow Q_OBJECT public: - explicit MainWindow(const QString name, QWidget *parent = nullptr); + explicit MainWindow(QWidget *parent = nullptr); static MainWindow *instance() { return instance_; }; void saveCurrentWindowSize(); @@ -149,6 +149,4 @@ private: LoadingIndicator *spinner_ = nullptr; JdenticonInterface *jdenticonInteface_ = nullptr; - - QString profile_; }; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 708fb7fd..55f666c1 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -89,6 +89,14 @@ UserSettings::load() cameraResolution_ = settings.value("user/camera_resolution", QString()).toString(); cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString(); 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(); } @@ -372,6 +380,56 @@ UserSettings::setCameraFrameRate(QString frameRate) 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 UserSettings::applyTheme() { @@ -436,14 +494,14 @@ UserSettings::save() settings.beginGroup("window"); settings.setValue("tray", tray_); settings.setValue("start_in_tray", startInTray_); - settings.endGroup(); + settings.endGroup(); // window settings.beginGroup("timeline"); settings.setValue("buttons", buttonsInTimeline_); settings.setValue("message_hover_highlight", messageHoverHighlight_); settings.setValue("enlarge_emoji_only_msg", enlargeEmojiOnlyMessages_); settings.setValue("max_width", timelineMaxWidth_); - settings.endGroup(); + settings.endGroup(); // timeline settings.setValue("avatar_circles", avatarCircles_); settings.setValue("decrypt_sidebar", decryptSidebar_); @@ -467,8 +525,16 @@ UserSettings::save() settings.setValue("camera_resolution", cameraResolution_); settings.setValue("camera_frame_rate", cameraFrameRate_); settings.setValue("use_stun_server", useStunServer_); + settings.setValue("currentProfile", profile_); + + 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(); + settings.endGroup(); // user settings.sync(); } diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index c699fd59..dd1e26d9 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -84,6 +84,12 @@ class UserSettings : public QObject bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged) Q_PROPERTY(bool shareKeysWithTrustedUsers READ shareKeysWithTrustedUsers WRITE 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: UserSettings(); @@ -95,7 +101,7 @@ public: Unavailable, Offline, }; - Q_ENUM(Presence); + Q_ENUM(Presence) void save(); void load(); @@ -128,6 +134,11 @@ public: void setCameraFrameRate(QString frameRate); void setUseStunServer(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_; } bool messageHoverHighlight() const { return messageHoverHighlight_; } @@ -161,6 +172,11 @@ public: QString cameraFrameRate() const { return cameraFrameRate_; } bool useStunServer() const { return useStunServer_; } 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: void groupViewStateChanged(bool state); @@ -191,6 +207,11 @@ signals: void cameraFrameRateChanged(QString frameRate); void useStunServerChanged(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: // Default to system theme if QT_QPA_PLATFORMTHEME var is set. @@ -226,6 +247,11 @@ private: QString cameraResolution_; QString cameraFrameRate_; bool useStunServer_; + QString profile_; + QString userId_; + QString accessToken_; + QString deviceId_; + QString homeserver_; }; class HorizontalLine : public QFrame diff --git a/src/main.cpp b/src/main.cpp index 5eeebb82..58bdda34 100644 --- a/src/main.cpp +++ b/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 qRegisterMetaType(); - // This is some hacky programming, but it's necessary (AFAIK?) to get the unique config name - // 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::setApplicationName("nheko"); QCoreApplication::setApplicationVersion(nheko::version); QCoreApplication::setOrganizationName("nheko"); QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); @@ -147,12 +125,36 @@ main(int argc, char *argv[]) } #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, argv, false, SingleApplication::Mode::User | SingleApplication::Mode::ExcludeAppPath | - SingleApplication::Mode::ExcludeAppVersion); + SingleApplication::Mode::ExcludeAppVersion, + 100, + userdata); QCommandLineParser parser; parser.addHelpOption(); @@ -194,14 +196,17 @@ main(int argc, char *argv[]) std::exit(1); } - QSettings settings; + UserSettings settings; + + if (parser.isSet(configName)) + settings.setProfile(parser.value(configName)); QFont font; - QString userFontFamily = settings.value("user/font_family", "").toString(); + QString userFontFamily = settings.font(); if (!userFontFamily.isEmpty()) { font.setFamily(userFontFamily); } - font.setPointSizeF(settings.value("user/font_size", font.pointSizeF()).toDouble()); + font.setPointSizeF(settings.fontSize()); app.setFont(font); @@ -216,13 +221,12 @@ main(int argc, char *argv[]) appTranslator.load(QLocale(), "nheko", "_", ":/translations"); app.installTranslator(&appTranslator); - MainWindow w{(appName == "nheko" ? "" : appName.remove("nheko-"))}; + MainWindow w; // Move the MainWindow to the center w.move(screenCenter(w.width(), w.height())); - if (!settings.value("user/window/start_in_tray", false).toBool() || - !settings.value("user/window/tray", true).toBool()) + if (!settings.startInTray() && !settings.tray()) w.show(); QObject::connect(&app, &QApplication::aboutToQuit, &w, [&w]() { diff --git a/third_party/SingleApplication-3.1.3.1/singleapplication.cpp b/third_party/SingleApplication-3.1.3.1/singleapplication.cpp deleted file mode 100644 index 9af38804..00000000 --- a/third_party/SingleApplication-3.1.3.1/singleapplication.cpp +++ /dev/null @@ -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 -#include -#include -#include -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) -#include -#else -#include -#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( 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::max() ); - QThread::sleep( 8 + static_cast ( static_cast ( 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; -} diff --git a/third_party/SingleApplication-3.1.3.1/.gitignore b/third_party/SingleApplication-3.2.0-dc8042b/.gitignore similarity index 100% rename from third_party/SingleApplication-3.1.3.1/.gitignore rename to third_party/SingleApplication-3.2.0-dc8042b/.gitignore diff --git a/third_party/SingleApplication-3.1.3.1/CHANGELOG.md b/third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md similarity index 88% rename from third_party/SingleApplication-3.1.3.1/CHANGELOG.md rename to third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md index 3662b0b2..e2ba290e 100644 --- a/third_party/SingleApplication-3.1.3.1/CHANGELOG.md +++ b/third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md @@ -3,6 +3,30 @@ Changelog 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__ --------- * CMake build system improvements @@ -38,18 +62,18 @@ __3.1.0a__ __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_ _Anton Filimonov_ _Jonas Kvinge_ - + __3.0.18__ ---------- * Fallback to standard QApplication class on iOS and Android systems where the library is not supported. - + * Added Build CI tests to verify the library builds successfully on Linux, Windows and MacOS across multiple Qt versions. _Anton Filimonov_ diff --git a/third_party/SingleApplication-3.1.3.1/CMakeLists.txt b/third_party/SingleApplication-3.2.0-dc8042b/CMakeLists.txt similarity index 53% rename from third_party/SingleApplication-3.1.3.1/CMakeLists.txt rename to third_party/SingleApplication-3.2.0-dc8042b/CMakeLists.txt index 85dba84c..ae1b1439 100644 --- a/third_party/SingleApplication-3.1.3.1/CMakeLists.txt +++ b/third_party/SingleApplication-3.2.0-dc8042b/CMakeLists.txt @@ -10,22 +10,28 @@ add_library(${PROJECT_NAME} STATIC ) 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_package(Qt5 COMPONENTS Network REQUIRED) -target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Network) +set(QT_COMPONENTS Core Network) +set(QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Core Qt${QT_DEFAULT_MAJOR_VERSION}::Network) if(QAPPLICATION_CLASS STREQUAL QApplication) - find_package(Qt5 COMPONENTS Widgets REQUIRED) - target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Widgets) + list(APPEND QT_COMPONENTS Widgets) + list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Widgets) elseif(QAPPLICATION_CLASS STREQUAL QGuiApplication) - find_package(Qt5 COMPONENTS Gui REQUIRED) - target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Gui) + list(APPEND QT_COMPONENTS Gui) + list(APPEND QT_LIBRARIES Qt${QT_DEFAULT_MAJOR_VERSION}::Gui) else() set(QAPPLICATION_CLASS QCoreApplication) - find_package(Qt5 COMPONENTS Core REQUIRED) - target_link_libraries(${PROJECT_NAME} PUBLIC Qt5::Core) endif() +find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS ${QT_COMPONENTS} REQUIRED) + +target_link_libraries(${PROJECT_NAME} PUBLIC ${QT_LIBRARIES}) + if(WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE advapi32) endif() diff --git a/third_party/SingleApplication-3.1.3.1/LICENSE b/third_party/SingleApplication-3.2.0-dc8042b/LICENSE similarity index 100% rename from third_party/SingleApplication-3.1.3.1/LICENSE rename to third_party/SingleApplication-3.2.0-dc8042b/LICENSE diff --git a/third_party/SingleApplication-3.1.3.1/README.md b/third_party/SingleApplication-3.2.0-dc8042b/README.md similarity index 90% rename from third_party/SingleApplication-3.1.3.1/README.md rename to third_party/SingleApplication-3.2.0-dc8042b/README.md index 3c36b557..457ab339 100644 --- a/third_party/SingleApplication-3.1.3.1/README.md +++ b/third_party/SingleApplication-3.2.0-dc8042b/README.md @@ -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) -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 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 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 --- ### Members ```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 @@ -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 used to notify the primary instance whenever a secondary instance had been 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 recognizes.* diff --git a/third_party/SingleApplication-3.1.3.1/SingleApplication b/third_party/SingleApplication-3.2.0-dc8042b/SingleApplication similarity index 100% rename from third_party/SingleApplication-3.1.3.1/SingleApplication rename to third_party/SingleApplication-3.2.0-dc8042b/SingleApplication diff --git a/third_party/SingleApplication-3.1.3.1/Windows.md b/third_party/SingleApplication-3.2.0-dc8042b/Windows.md similarity index 100% rename from third_party/SingleApplication-3.1.3.1/Windows.md rename to third_party/SingleApplication-3.2.0-dc8042b/Windows.md diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp new file mode 100644 index 00000000..276ceee9 --- /dev/null +++ b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp @@ -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 +#include +#include + +#include "singleapplication.h" +#include "singleapplication_p.h" + +/** + * @brief Constructor. Checks and fires up LocalServer or closes the program + * if another instance already exists + * @param argc + * @param argv + * @param allowSecondary Whether to enable secondary instance support + * @param options Optional flags to toggle specific behaviour + * @param timeout Maximum time blocking functions are allowed during app load + */ +SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, QString userData ) + : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) ) +{ + Q_D( SingleApplication ); + +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + // On Android and iOS since the library is not supported fallback to + // standard QApplication behaviour by simply returning at this point. + qWarning() << "SingleApplication is not supported on Android and iOS systems."; + return; +#endif + + // Store the current mode of the program + d->options = options; + + // Add any unique user data + if ( ! userData.isEmpty() ) + d->addAppData( userData ); + + // Generating an application ID used for identifying the shared memory + // block and QLocalServer + d->genBlockServerName(); + + // To mitigate QSharedMemory issues with large amount of processes + // attempting to attach at the same time + SingleApplicationPrivate::randomSleep(); + +#ifdef Q_OS_UNIX + // By explicitly attaching it and then deleting it we make sure that the + // memory is deleted even after the process has crashed on Unix. + d->memory = new QSharedMemory( d->blockServerName ); + d->memory->attach(); + delete d->memory; +#endif + // Guarantee thread safe behaviour with a shared memory block. + d->memory = new QSharedMemory( d->blockServerName ); + + // Create a shared memory block + if( d->memory->create( sizeof( InstancesInfo ) )){ + // Initialize the shared memory block + if( ! d->memory->lock() ){ + qCritical() << "SingleApplication: Unable to lock memory block after create."; + abortSafely(); + } + d->initializeMemoryBlock(); + } else { + if( d->memory->error() == QSharedMemory::AlreadyExists ){ + // Attempt to attach to the memory segment + if( ! d->memory->attach() ){ + qCritical() << "SingleApplication: Unable to attach to shared memory block."; + abortSafely(); + } + if( ! d->memory->lock() ){ + qCritical() << "SingleApplication: Unable to lock memory block after attach."; + abortSafely(); + } + } else { + qCritical() << "SingleApplication: Unable to create block."; + abortSafely(); + } + } + + auto *inst = static_cast( d->memory->data() ); + QElapsedTimer time; + time.start(); + + // Make sure the shared memory block is initialised and in consistent state + while( true ){ + // If the shared memory block's checksum is valid continue + if( d->blockChecksum() == inst->checksum ) break; + + // If more than 5s have elapsed, assume the primary instance crashed and + // assume it's position + if( time.elapsed() > 5000 ){ + qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure."; + d->initializeMemoryBlock(); + } + + // Otherwise wait for a random period and try again. The random sleep here + // limits the probability of a collision between two racing apps and + // allows the app to initialise faster + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory for random wait."; + qDebug() << d->memory->errorString(); + } + SingleApplicationPrivate::randomSleep(); + if( ! d->memory->lock() ){ + qCritical() << "SingleApplication: Unable to lock memory after random wait."; + abortSafely(); + } + } + + if( inst->primary == false ){ + d->startPrimary(); + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory after primary start."; + qDebug() << d->memory->errorString(); + } + return; + } + + // Check if another instance can be started + if( allowSecondary ){ + d->startSecondary(); + if( d->options & Mode::SecondaryNotification ){ + d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance ); + } + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory after secondary start."; + qDebug() << d->memory->errorString(); + } + return; + } + + if( ! d->memory->unlock() ){ + qDebug() << "SingleApplication: Unable to unlock memory at end of execution."; + qDebug() << d->memory->errorString(); + } + + d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance ); + + delete d; + + ::exit( EXIT_SUCCESS ); +} + +SingleApplication::~SingleApplication() +{ + Q_D( SingleApplication ); + delete d; +} + +/** + * Checks if the current application instance is primary. + * @return Returns true if the instance is primary, false otherwise. + */ +bool SingleApplication::isPrimary() +{ + 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(); +} diff --git a/third_party/SingleApplication-3.1.3.1/singleapplication.h b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h similarity index 96% rename from third_party/SingleApplication-3.1.3.1/singleapplication.h rename to third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h index fd806a3d..d39a6614 100644 --- a/third_party/SingleApplication-3.1.3.1/singleapplication.h +++ b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h @@ -85,7 +85,7 @@ public: * Usually 4*timeout would be the worst case (fail) scenario. * @see See the corresponding QAPPLICATION_CLASS constructor for reference */ - explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000 ); + explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, QString userData = QString() ); ~SingleApplication() override; /** @@ -133,6 +133,12 @@ public: */ bool sendMessage( const QByteArray &message, int timeout = 100 ); + /** + * @brief Get the set user data. + * @returns {QStringList} + */ + QStringList userData(); + Q_SIGNALS: void instanceStarted(); void receivedMessage( quint32 instanceId, QByteArray message ); @@ -140,6 +146,7 @@ Q_SIGNALS: private: SingleApplicationPrivate *d_ptr; Q_DECLARE_PRIVATE(SingleApplication) + void abortSafely(); }; Q_DECLARE_OPERATORS_FOR_FLAGS(SingleApplication::Options) diff --git a/third_party/SingleApplication-3.1.3.1/singleapplication.pri b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.pri similarity index 100% rename from third_party/SingleApplication-3.1.3.1/singleapplication.pri rename to third_party/SingleApplication-3.2.0-dc8042b/singleapplication.pri diff --git a/third_party/SingleApplication-3.1.3.1/singleapplication_p.cpp b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp similarity index 67% rename from third_party/SingleApplication-3.1.3.1/singleapplication_p.cpp rename to third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp index 705609f2..1ab58c23 100644 --- a/third_party/SingleApplication-3.1.3.1/singleapplication_p.cpp +++ b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp @@ -33,12 +33,20 @@ #include #include +#include #include #include +#include #include #include #include +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) +#include +#else +#include +#endif + #include "singleapplication.h" #include "singleapplication_p.h" @@ -49,6 +57,9 @@ #endif #ifdef Q_OS_WIN + #ifndef NOMINMAX + #define NOMINMAX 1 + #endif #include #include #endif @@ -59,20 +70,20 @@ SingleApplicationPrivate::SingleApplicationPrivate( SingleApplication *q_ptr ) server = nullptr; socket = nullptr; memory = nullptr; - instanceNumber = -1; + instanceNumber = 0; } SingleApplicationPrivate::~SingleApplicationPrivate() { - if( socket != nullptr ) { + if( socket != nullptr ){ socket->close(); delete socket; } - if( memory != nullptr ) { + if( memory != nullptr ){ memory->lock(); auto *inst = static_cast(memory->data()); - if( server != nullptr ) { + if( server != nullptr ){ server->close(); delete server; inst->primary = false; @@ -106,7 +117,7 @@ QString SingleApplicationPrivate::getUsername() struct passwd *pw = getpwuid( uid ); if( pw ) username = QString::fromLocal8Bit( pw->pw_name ); - if ( username.isEmpty() ) { + if ( username.isEmpty() ){ #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) username = QString::fromLocal8Bit( qgetenv( "USER" ) ); #else @@ -125,11 +136,14 @@ void SingleApplicationPrivate::genBlockServerName() appData.addData( SingleApplication::app_t::organizationName().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() ); } - if( ! (options & SingleApplication::Mode::ExcludeAppPath) ) { + if( ! (options & SingleApplication::Mode::ExcludeAppPath) ){ #ifdef Q_OS_WIN appData.addData( SingleApplication::app_t::applicationFilePath().toLower().toUtf8() ); #else @@ -138,7 +152,7 @@ void SingleApplicationPrivate::genBlockServerName() } // 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() ); } @@ -147,7 +161,7 @@ void SingleApplicationPrivate::genBlockServerName() blockServerName = appData.result().toBase64().replace("/", "_"); } -void SingleApplicationPrivate::initializeMemoryBlock() +void SingleApplicationPrivate::initializeMemoryBlock() const { auto *inst = static_cast( memory->data() ); inst->primary = false; @@ -159,8 +173,14 @@ void SingleApplicationPrivate::initializeMemoryBlock() void SingleApplicationPrivate::startPrimary() { - Q_Q(SingleApplication); + // Reset the number of connections + auto *inst = static_cast ( memory->data() ); + inst->primary = true; + inst->primaryPid = QCoreApplication::applicationPid(); + qstrncpy( inst->primaryUser, getUsername().toUtf8().data(), sizeof(inst->primaryUser) ); + inst->checksum = blockChecksum(); + instanceNumber = 0; // Successful creation means that no main process exists // So we start a QLocalServer to listen for connections QLocalServer::removeServer( blockServerName ); @@ -168,10 +188,10 @@ void SingleApplicationPrivate::startPrimary() // Restrict access to the socket according to the // SingleApplication::Mode::User flag on User level or no restrictions - if( options & SingleApplication::Mode::User ) { - server->setSocketOptions( QLocalServer::UserAccessOption ); + if( options & SingleApplication::Mode::User ){ + server->setSocketOptions( QLocalServer::UserAccessOption ); } else { - server->setSocketOptions( QLocalServer::WorldAccessOption ); + server->setSocketOptions( QLocalServer::WorldAccessOption ); } server->listen( blockServerName ); @@ -181,87 +201,95 @@ void SingleApplicationPrivate::startPrimary() this, &SingleApplicationPrivate::slotConnectionEstablished ); - - // Reset the number of connections - auto *inst = static_cast ( 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() { + auto *inst = static_cast ( 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 // connected. - if( socket == nullptr ) { + if( socket == nullptr ){ socket = new QLocalSocket(); } - // If already connected - we are done; - if( socket->state() == QLocalSocket::ConnectedState ) - return; + if( socket->state() == QLocalSocket::ConnectedState ) return true; - // If not connect - if( socket->state() == QLocalSocket::UnconnectedState || - socket->state() == QLocalSocket::ClosingState ) { - socket->connectToServer( blockServerName ); - } + if( socket->state() != QLocalSocket::ConnectedState ){ + + while( true ){ + randomSleep(); - // Wait for being connected - if( socket->state() == QLocalSocket::ConnectingState ) { - socket->waitForConnected( msecs ); + if( socket->state() != QLocalSocket::ConnectingState ) + socket->connectToServer( blockServerName ); + + if( socket->state() == QLocalSocket::ConnectingState ){ + socket->waitForConnected( static_cast(msecs - time.elapsed()) ); + } + + // If connected break out of the loop + if( socket->state() == QLocalSocket::ConnectedState ) break; + + // If elapsed time since start is longer than the method timeout return + if( time.elapsed() >= msecs ) return false; + } } // Initialisation message according to the SingleApplication protocol - if( socket->state() == QLocalSocket::ConnectedState ) { - // Notify the parent that a new instance had been started; - QByteArray initMsg; - QDataStream writeStream(&initMsg, QIODevice::WriteOnly); + QByteArray initMsg; + QDataStream writeStream(&initMsg, QIODevice::WriteOnly); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - writeStream.setVersion(QDataStream::Qt_5_6); + writeStream.setVersion(QDataStream::Qt_5_6); #endif - writeStream << blockServerName.toLatin1(); - writeStream << static_cast(connectionType); - writeStream << instanceNumber; - quint16 checksum = qChecksum(initMsg.constData(), static_cast(initMsg.length())); - writeStream << checksum; + writeStream << blockServerName.toLatin1(); + writeStream << static_cast(connectionType); + writeStream << instanceNumber; +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + quint16 checksum = qChecksum(QByteArray(initMsg, static_cast(initMsg.length()))); +#else + quint16 checksum = qChecksum(initMsg.constData(), static_cast(initMsg.length())); +#endif + writeStream << checksum; - // The header indicates the message length that follows - QByteArray header; - QDataStream headerStream(&header, QIODevice::WriteOnly); + // The header indicates the message length that follows + QByteArray header; + QDataStream headerStream(&header, QIODevice::WriteOnly); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - headerStream.setVersion(QDataStream::Qt_5_6); + headerStream.setVersion(QDataStream::Qt_5_6); #endif - headerStream << static_cast ( initMsg.length() ); + headerStream << static_cast ( initMsg.length() ); - socket->write( header ); - socket->write( initMsg ); - socket->flush(); - socket->waitForBytesWritten( msecs ); - } + socket->write( header ); + socket->write( initMsg ); + bool result = socket->waitForBytesWritten( static_cast(msecs - time.elapsed()) ); + socket->flush(); + return result; } -quint16 SingleApplicationPrivate::blockChecksum() +quint16 SingleApplicationPrivate::blockChecksum() const { - return qChecksum( - static_cast ( memory->data() ), - offsetof( InstancesInfo, checksum ) - ); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + quint16 checksum = qChecksum(QByteArray(static_cast(memory->constData()), offsetof(InstancesInfo, checksum))); +#else + quint16 checksum = qChecksum(static_cast(memory->constData()), offsetof(InstancesInfo, checksum)); +#endif + return checksum; } -qint64 SingleApplicationPrivate::primaryPid() +qint64 SingleApplicationPrivate::primaryPid() const { qint64 pid; @@ -273,7 +301,7 @@ qint64 SingleApplicationPrivate::primaryPid() return pid; } -QString SingleApplicationPrivate::primaryUser() +QString SingleApplicationPrivate::primaryUser() const { QByteArray username; @@ -294,7 +322,7 @@ void SingleApplicationPrivate::slotConnectionEstablished() connectionMap.insert(nextConnSocket, ConnectionInfo()); QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, - [nextConnSocket, this]() { + [nextConnSocket, this](){ auto &info = connectionMap[nextConnSocket]; Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId ); } @@ -308,9 +336,9 @@ void SingleApplicationPrivate::slotConnectionEstablished() ); QObject::connect(nextConnSocket, &QLocalSocket::readyRead, - [nextConnSocket, this]() { + [nextConnSocket, this](){ auto &info = connectionMap[nextConnSocket]; - switch(info.stage) { + switch(info.stage){ case StageHeader: readInitMessageHeader(nextConnSocket); break; @@ -329,11 +357,11 @@ void SingleApplicationPrivate::slotConnectionEstablished() void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock ) { - if (!connectionMap.contains( sock )) { + if (!connectionMap.contains( sock )){ return; } - if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ) { + if( sock->bytesAvailable() < ( qint64 )sizeof( quint64 ) ){ return; } @@ -350,7 +378,7 @@ void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock ) info.stage = StageBody; info.msgLen = msgLen; - if ( sock->bytesAvailable() >= (qint64) msgLen ) { + if ( sock->bytesAvailable() >= (qint64) msgLen ){ readInitMessageBody( sock ); } } @@ -359,12 +387,12 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) { Q_Q(SingleApplication); - if (!connectionMap.contains( sock )) { + if (!connectionMap.contains( sock )){ return; } ConnectionInfo &info = connectionMap[sock]; - if( sock->bytesAvailable() < ( qint64 )info.msgLen ) { + if( sock->bytesAvailable() < ( qint64 )info.msgLen ){ return; } @@ -394,13 +422,17 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) quint16 msgChecksum = 0; readStream >> msgChecksum; - const quint16 actualChecksum = qChecksum( msgBytes.constData(), static_cast( msgBytes.length() - sizeof( quint16 ) ) ); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const quint16 actualChecksum = qChecksum(QByteArray(msgBytes, static_cast(msgBytes.length() - sizeof(quint16)))); +#else + const quint16 actualChecksum = qChecksum(msgBytes.constData(), static_cast(msgBytes.length() - sizeof(quint16))); +#endif bool isValid = readStream.status() == QDataStream::Ok && QLatin1String(latin1Name) == blockServerName && msgChecksum == actualChecksum; - if( !isValid ) { + if( !isValid ){ sock->close(); return; } @@ -415,7 +447,7 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock ) Q_EMIT q->instanceStarted(); } - if (sock->bytesAvailable() > 0) { + if (sock->bytesAvailable() > 0){ Q_EMIT this->slotDataAvailable( sock, instanceId ); } } @@ -431,3 +463,23 @@ void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedS if( closedSocket->bytesAvailable() > 0 ) 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::max() ); + QThread::msleep( 8 + static_cast ( static_cast ( qrand() ) / RAND_MAX * 10 )); +#endif +} + +void SingleApplicationPrivate::addAppData(const QString &data) +{ + appDataList.push_back(data); +} + +QStringList SingleApplicationPrivate::appData() const +{ + return appDataList; +} diff --git a/third_party/SingleApplication-3.1.3.1/singleapplication_p.h b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h similarity index 87% rename from third_party/SingleApplication-3.1.3.1/singleapplication_p.h rename to third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h index 29ba346b..c49a46dd 100644 --- a/third_party/SingleApplication-3.1.3.1/singleapplication_p.h +++ b/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h @@ -41,8 +41,8 @@ struct InstancesInfo { bool primary; quint32 secondary; qint64 primaryPid; - quint16 checksum; char primaryUser[128]; + quint16 checksum; // Must be the last field }; struct ConnectionInfo { @@ -70,17 +70,20 @@ public: SingleApplicationPrivate( SingleApplication *q_ptr ); ~SingleApplicationPrivate() override; - QString getUsername(); + static QString getUsername(); void genBlockServerName(); - void initializeMemoryBlock(); + void initializeMemoryBlock() const; void startPrimary(); void startSecondary(); - void connectToPrimary(int msecs, ConnectionType connectionType ); - quint16 blockChecksum(); - qint64 primaryPid(); - QString primaryUser(); + bool connectToPrimary( int msecs, ConnectionType connectionType ); + quint16 blockChecksum() const; + qint64 primaryPid() const; + QString primaryUser() const; void readInitMessageHeader(QLocalSocket *socket); void readInitMessageBody(QLocalSocket *socket); + static void randomSleep(); + void addAppData(const QString &data); + QStringList appData() const; SingleApplication *q_ptr; QSharedMemory *memory; @@ -90,6 +93,7 @@ public: QString blockServerName; SingleApplication::Options options; QMap connectionMap; + QStringList appDataList; public Q_SLOTS: void slotConnectionEstablished();