single instance handled

This commit is contained in:
Sale Djenic 2022-03-14 11:09:17 +01:00 committed by Stefan Dunca
parent a9f179cab3
commit e3b0610b65
6 changed files with 152 additions and 12 deletions

View File

@ -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>("AppWindow", 0 , 1, "AppWindow");
qmlRegisterType<AppWindow>("DotherSide", 0 , 1, "StatusWindow");
qmlRegisterType<StatusSyntaxHighlighterHelper>("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>("AppWindow", 0 , 1, "AppWindow");
qmlRegisterType<AppWindow>("DotherSide", 0 , 1, "StatusWindow");
qmlRegisterType<StatusSyntaxHighlighterHelper>("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<SingleInstance>(md5DataDir, openUri);
if (!singleInstance->isFirstInstance())
{
auto err = "Terminating the app as the second instance";
throw std::runtime_error(err);
}
auto rootModule = Injector.create<Modules::ModuleBuilder>()();
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())
{
if(obj)
{
AppWindow* appWindow = qobject_cast<AppWindow*>(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();
}

View File

@ -35,6 +35,13 @@ bool AppWindow::isFullScreen() const
return m_isFullScreen;
}
void AppWindow::makeTheAppActive()
{
show();
raise();
requestActivate();
}
void AppWindow::removeTitleBar()
{
#ifdef Q_OS_MACOS

View File

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

View File

@ -0,0 +1,64 @@
#include "SingleInstance.h"
#include <QtNetwork>
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();
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <QtCore>
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;
};
}

View File

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