diff --git a/src-cpp-structure/projects/App/Boot/AppController.cpp b/src-cpp-structure/projects/App/Boot/AppController.cpp index 05ca76d51f..79892454aa 100644 --- a/src-cpp-structure/projects/App/Boot/AppController.cpp +++ b/src-cpp-structure/projects/App/Boot/AppController.cpp @@ -3,6 +3,7 @@ #include "DI.h" #include "../Core/Engine.h" #include "../Core/StatusSyntaxHighlighter.h" +#include "../Core/SingleInstance.h" #include "../Common/Utils.h" #include "../Global/LocalAppSettings.h" #include "../Global/LocalAccountSettings.h" @@ -30,6 +31,14 @@ AppController::AppController() Utils::ensureDirectories(); } +void registerTypes() +{ + // Once we fully move to c++ we should include the following line instead the line below it (it's here just to align with the current qml files). + // qmlRegisterType("AppWindow", 0 , 1, "AppWindow"); + qmlRegisterType("DotherSide", 0 , 1, "StatusWindow"); + qmlRegisterType("DotherSide", 0, 1, "StatusSyntaxHighlighter"); +} + void registerResources() { Engine::instance()->addImportPath("qrc:/./StatusQ/src"); @@ -51,10 +60,7 @@ int AppController::exec(int& argc, char** argv) { int code; -// Once we fully move to c++ we should include the following line instead the line below it (it's here just to align with the current qml files). -// qmlRegisterType("AppWindow", 0 , 1, "AppWindow"); - qmlRegisterType("DotherSide", 0 , 1, "StatusWindow"); - qmlRegisterType("DotherSide", 0, 1, "StatusSyntaxHighlighter"); + registerTypes(); try { @@ -69,21 +75,49 @@ int AppController::exec(int& argc, char** argv) // app.installTranslator(&translator); // } + auto md5DataDir = QString(QCryptographicHash::hash(Utils::defaultDataDir().toLatin1(), QCryptographicHash::Md5).toHex()); + auto openUri = ""; // CLI uri should be used here ("status-im:// URI to open a chat or other") + auto singleInstance = std::make_unique(md5DataDir, openUri); + + if (!singleInstance->isFirstInstance()) + { + auto err = "Terminating the app as the second instance"; + throw std::runtime_error(err); + } + auto rootModule = Injector.create()(); rootModule->load(); registerResources(); + AppWindow* appWindow = nullptr; QString qmlFile = QStringLiteral("qrc:/main.qml"); - Engine::create(qmlFile); - QObject::connect(Engine::instance(), &Engine::objectCreated, &app, - [url = qmlFile](QObject* obj, const QUrl& objUrl) { - if(!obj && url == objUrl.toString()) + + auto handleAppWinCreation = [url = qmlFile, &appWindow, &singleInstance](QObject* obj, const QUrl& objUrl) { + if(url == objUrl.toString()) { - auto err = "Failed to create: " + url; - throw std::runtime_error(err.toStdString()); + if(obj) + { + AppWindow* appWindow = qobject_cast(obj); + QObject::connect(singleInstance.get(), &SingleInstance::secondInstanceDetected, [appWindow](){ + appWindow->makeTheAppActive(); + }); + + QObject::connect(singleInstance.get(), &SingleInstance::eventReceived, [](const QString& eventStr){ + qInfo() << "Received event: " << eventStr; + // We need to handle it here. + }); + } + else + { + auto err = "Failed to create: " + url; + throw std::runtime_error(err.toStdString()); + } } - }); + }; + + Engine::create(qmlFile); + QObject::connect(Engine::instance(), &Engine::objectCreated, &app, handleAppWinCreation); code = app.exec(); } diff --git a/src-cpp-structure/projects/App/Boot/AppWindow.cpp b/src-cpp-structure/projects/App/Boot/AppWindow.cpp index 070f3cc2f9..3ad24fba0e 100644 --- a/src-cpp-structure/projects/App/Boot/AppWindow.cpp +++ b/src-cpp-structure/projects/App/Boot/AppWindow.cpp @@ -35,6 +35,13 @@ bool AppWindow::isFullScreen() const return m_isFullScreen; } +void AppWindow::makeTheAppActive() +{ + show(); + raise(); + requestActivate(); +} + void AppWindow::removeTitleBar() { #ifdef Q_OS_MACOS diff --git a/src-cpp-structure/projects/App/Boot/AppWindow.h b/src-cpp-structure/projects/App/Boot/AppWindow.h index f81493dee7..338ca5cc67 100644 --- a/src-cpp-structure/projects/App/Boot/AppWindow.h +++ b/src-cpp-structure/projects/App/Boot/AppWindow.h @@ -21,6 +21,7 @@ namespace Status Q_INVOKABLE void toggleFullScreen(); bool isFullScreen() const; + void makeTheAppActive(); Q_INVOKABLE void updatePosition() { auto point = QPoint(screen()->geometry().center().x() - geometry().width() / 2, diff --git a/src-cpp-structure/projects/App/Core/SingleInstance.cpp b/src-cpp-structure/projects/App/Core/SingleInstance.cpp new file mode 100644 index 0000000000..8739dff591 --- /dev/null +++ b/src-cpp-structure/projects/App/Core/SingleInstance.cpp @@ -0,0 +1,64 @@ +#include "SingleInstance.h" + +#include + +using namespace Status; + +namespace { + const int ReadWriteTimeoutMs = 1000; +} + +SingleInstance::SingleInstance(const QString &uniqueName, const QString &eventStr, QObject *parent) + : QObject(parent) + , m_localServer(new QLocalServer(this)) +{ + QString socketName = uniqueName; + +#ifndef Q_OS_WIN + socketName = QString("/tmp/%1").arg(socketName); +#endif + + QLocalSocket localSocket; + localSocket.connectToServer(socketName); + + // the first instance start will be delayed by this timeout (ms) to ensure there are no other instances. + // note: this is an ad-hoc timeout value selected based on prior experience. + if (!localSocket.waitForConnected(100)) { + connect(m_localServer, &QLocalServer::newConnection, this, &SingleInstance::handleNewConnection); + // on *nix a crashed process will leave /tmp/xyz file preventing to start a new server. + // therefore, if we were unable to connect, then we assume the server died and we need to clean up. + // p.s. on Windows, this function does nothing. + QLocalServer::removeServer(socketName); + if (!m_localServer->listen(socketName)) { + qWarning() << "QLocalServer::listen(" << socketName << ") failed"; + } + } else if (!eventStr.isEmpty()) { + localSocket.write(eventStr.toUtf8() + '\n'); + localSocket.waitForBytesWritten(ReadWriteTimeoutMs); + } +} + +SingleInstance::~SingleInstance() +{ + if (m_localServer->isListening()) { + m_localServer->close(); + } +} + +bool SingleInstance::isFirstInstance() const +{ + return m_localServer->isListening(); +} + +void SingleInstance::handleNewConnection() +{ + emit secondInstanceDetected(); + + auto socket = m_localServer->nextPendingConnection(); + if (socket->waitForReadyRead(ReadWriteTimeoutMs) && socket->canReadLine()) { + auto event = socket->readLine(); + emit eventReceived(QString::fromUtf8(event)); + } + + socket->deleteLater(); +} diff --git a/src-cpp-structure/projects/App/Core/SingleInstance.h b/src-cpp-structure/projects/App/Core/SingleInstance.h new file mode 100644 index 0000000000..d933d84f00 --- /dev/null +++ b/src-cpp-structure/projects/App/Core/SingleInstance.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +class QLocalServer; + +namespace Status { + + class SingleInstance : public QObject + { + Q_OBJECT + + public: + // uniqueName - the name of named pipe + // eventStr - optional event to send if another instance is detected + explicit SingleInstance(const QString& uniqueName, const QString& eventStr, QObject* parent = nullptr); + ~SingleInstance() override; + + bool isFirstInstance() const; + + signals: + void secondInstanceDetected(); + void eventReceived(const QString& eventStr); + + private slots: + void handleNewConnection(); + + private: + QLocalServer* m_localServer; + }; +} diff --git a/ui/main.qml b/ui/main.qml index 131d0d6bf7..9598a368f5 100644 --- a/ui/main.qml +++ b/ui/main.qml @@ -174,7 +174,10 @@ StatusWindow { } Connections { - target: singleInstance + // This handling should be part of backend code, but because of compatibility with the current Nim App + // since c++ and Nim app are sharing the same/identical qml code we are still not allowed to remove this + // completely, and that's why we have this `target` set to null in case of c++ app. + target: !Constants.isCppApp? singleInstance : null onSecondInstanceDetected: { console.log("User attempted to run the second instance of the application")