1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2024-10-28 08:49:35 +01:00

main: use IPC to terminate secondary client during re-install(win)

To coordinate the MSI installer with Jami's close-to-tray feature,
a custom action was used to kill QtWebEngineProcess and Jami, prior
to the file installation step. The close-to-tray feature makes it
so the only way to terminate the app is via the systray context
menu.

This patch harnesses the IPC mechanism used by the run-guard, and
adds a command-line option('--term') used to signal the secondary
process and provoke graceful termination.

The benefits are:
- the app can save data before closing
- system tray icons aren't left dangling after updates
- QtWebEngineProcess is guaranteed to be terminated during install

Gitlab: #654
Gitlab: #543
Change-Id: I79421eeab49c9ec0826010af99a364471bb81d1a
This commit is contained in:
Andreas Traczyk 2022-01-21 14:42:35 -05:00
parent cab5a2223b
commit 5613a81ada
10 changed files with 286 additions and 226 deletions

View file

@ -97,7 +97,7 @@ set(COMMON_SOURCES
${SRC_DIR}/bannedlistmodel.cpp
${SRC_DIR}/accountlistmodel.cpp
${SRC_DIR}/networkmanager.cpp
${SRC_DIR}/runguard.cpp
${SRC_DIR}/instancemanager.cpp
${SRC_DIR}/updatemanager.cpp
${SRC_DIR}/main.cpp
${SRC_DIR}/smartlistmodel.cpp
@ -152,7 +152,7 @@ set(COMMON_HEADERS
${SRC_DIR}/bannedlistmodel.h
${SRC_DIR}/version.h
${SRC_DIR}/accountlistmodel.h
${SRC_DIR}/runguard.h
${SRC_DIR}/instancemanager.h
${SRC_DIR}/rendermanager.h
${SRC_DIR}/connectivitymonitor.h
${SRC_DIR}/jamiavatartheme.h

View file

@ -6,6 +6,8 @@
<?define Name="Jami (BETA)" ?>
<?endif ?>
<?define ExeName="Jami"?>
<?if $(var.Configuration) = Release ?>
<?define ReleaseDir="..\x64\Release"?>
<?else?>

View file

@ -5,7 +5,7 @@
<Package InstallerVersion="301" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes"/>
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" MaximumUncompressedMediaSize="4" />
<!--Disables interaction of the package with the Restart Manager.-->
<Property Id="MSIRESTARTMANAGERCONTROL" Value="Disable" />
@ -44,7 +44,7 @@
<WixVariable Id="WixUIDialogBmp" Value="main-banner.bmp" />
<WixVariable Id="WixUISupportPerUser" Value="0" />
<CustomAction Id="removeOldJamiFiles"
<CustomAction Id="RemoveOldJamiFiles"
Directory="APPLICATIONFOLDER"
ExeCommand="cmd /c &quot;del vc_redist.x64.exe; del uninstall.exe; del WinSparkle.dll;&quot;"
Execute="deferred"
@ -52,8 +52,9 @@
HideTarget="no"
Impersonate="no"/>
<Property Id="QtExecCmdLine" Value='"[WindowsFolder]\System32\taskkill.exe" /F /IM QtWebEngineProcess.exe /IM Jami.exe'/>
<CustomAction Id="JamiProcesses.TaskKill"
<Property Id="QtExecCmdLine"
Value='"[APPLICATIONFOLDER]/$(var.ExeName).exe" --term'/>
<CustomAction Id="TerminateAppProcess"
BinaryKey="WixCA"
DllEntry="CAQuietExec"
Execute="immediate"
@ -123,8 +124,8 @@
</InstallUISequence>
</UI>
<InstallExecuteSequence>
<Custom Action='JamiProcesses.TaskKill' Before='InstallValidate'/>
<Custom Action="removeOldJamiFiles" After="RemoveFiles" />
<Custom Action='TerminateAppProcess' Before='InstallValidate'/>
<Custom Action="RemoveOldJamiFiles" After="RemoveFiles" />
<Custom Action="LaunchApplication_nonUI" After="InstallFinalize"> WIXNONUILAUNCH </Custom>
<Custom Action="Overwrite_WixSetDefaultPerMachineFolder" After="WixSetDefaultPerMachineFolder" />
</InstallExecuteSequence>

View file

@ -79,7 +79,7 @@
<File Id="filD6887AD9110E4A8D49143C9A8F0B5843" KeyPath="yes" Source="$(var.UcrtDir)\ucrtbase.dll" />
</Component>
<Component Id="cmp9CFEE34E3A162AB05264E8B756EC1DEC" Directory="APPLICATIONFOLDER" Guid="*">
<File Id="fileMain.exe" KeyPath="yes" Source="$(var.ReleaseDir)\Jami.exe" />
<File Id="fileMain.exe" KeyPath="yes" Source="$(var.ReleaseDir)\$(var.ExeName).exe" />
</Component>
</ComponentGroup>
</Fragment>

208
src/instancemanager.cpp Normal file
View file

@ -0,0 +1,208 @@
/*
* Copyright (C) 2019-2022 Savoir-faire Linux Inc.
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "instancemanager.h"
#include "mainapplication.h"
#include <QCryptographicHash>
#include <QLocalSocket>
#include <QLocalServer>
#include <QSharedMemory>
#include <QSystemSemaphore>
static QString
generateKeyHash(const QString& key, const QString& salt)
{
QByteArray data;
data.append(key.toUtf8());
data.append(salt.toUtf8());
data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex();
return data;
}
class InstanceManager::Impl : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(Impl)
public:
Impl(const QString& key, MainApplication* mainApp)
: QObject(nullptr)
, mainAppInstance_(mainApp)
, key_(key)
, memLockKey_(generateKeyHash(key, "_memLockKey"))
, sharedmemKey_(generateKeyHash(key, "_sharedmemKey"))
, sharedMem_(sharedmemKey_)
, memLock_(memLockKey_, 1)
{}
~Impl() = default;
bool tryToRun()
{
if (isAnotherRunning()) {
// This is a secondary instance,
// connect to the primary instance to trigger a restore
// then fail.
if (connectToLocal()) {
return false;
}
// If not connected, this means that the server doesn't exist
// and the app can be relaunched (can be the case after a client crash or Ctrl+C)
}
memLock_.acquire();
const bool result = sharedMem_.create(sizeof(quint64));
memLock_.release();
if (!result) {
release();
return false;
}
// This is the primary instance,
// listen for subsequent instances.
QLocalServer::removeServer(key_);
server_ = new QLocalServer();
server_->setSocketOptions(QLocalServer::UserAccessOption);
server_->listen(key_);
QObject::connect(server_,
&QLocalServer::newConnection,
this,
&Impl::handleIncomingConnection);
return true;
};
void tryToKill()
{
if (!isAnotherRunning()) {
return;
}
// This is a secondary instance, connect to the primary
// instance to trigger a termination then fail.
if (!connectToLocal()) {
return;
}
socket_->write(reinterpret_cast<const char*>(terminateSeq_.data()), 4);
socket_->waitForBytesWritten();
};
void release()
{
memLock_.acquire();
if (sharedMem_.isAttached())
sharedMem_.detach();
memLock_.release();
};
private Q_SLOTS:
bool connectToLocal()
{
if (!socket_)
socket_ = new QLocalSocket();
if (!socket_)
return false;
if (socket_->state() == QLocalSocket::UnconnectedState
|| socket_->state() == QLocalSocket::ClosingState) {
socket_->connectToServer(key_);
}
if (socket_->state() == QLocalSocket::ConnectingState) {
socket_->waitForConnected(connectionTimeoutMs_);
}
return socket_->state() == QLocalSocket::ConnectedState;
}
void handleIncomingConnection()
{
connection_ = new QLocalSocket(this);
connection_ = server_->nextPendingConnection();
connect(connection_, &QLocalSocket::readyRead, this, [this] {
QLocalSocket* clientSocket = (QLocalSocket*) sender();
QByteArray recievedData;
recievedData = clientSocket->readAll();
if (recievedData == terminateSeq_) {
qWarning() << "Received terminate signal.";
mainAppInstance_->quit();
}
});
// Restore primary instance
mainAppInstance_->restoreApp();
};
private:
MainApplication* mainAppInstance_;
const QString key_;
const QString memLockKey_;
const QString sharedmemKey_;
QSharedMemory sharedMem_;
QSystemSemaphore memLock_;
QLocalSocket* socket_ {nullptr};
QLocalServer* server_ {nullptr};
QLocalSocket* connection_ {nullptr};
const int connectionTimeoutMs_ {2000};
const QByteArray terminateSeq_ {QByteArrayLiteral("\xde\xad\xbe\xef")};
bool isAnotherRunning()
{
if (sharedMem_.isAttached())
return false;
memLock_.acquire();
const bool isRunning = sharedMem_.attach();
if (isRunning)
sharedMem_.detach();
memLock_.release();
return isRunning;
};
};
InstanceManager::InstanceManager(MainApplication* mainApp)
: QObject(mainApp)
{
QCryptographicHash appData(QCryptographicHash::Sha256);
appData.addData(QApplication::applicationName().toUtf8());
appData.addData(QApplication::organizationDomain().toUtf8());
pimpl_ = std::make_unique<Impl>(appData.result(), mainApp);
}
InstanceManager::~InstanceManager()
{
pimpl_->release();
}
bool
InstanceManager::tryToRun()
{
return pimpl_->tryToRun();
}
void
InstanceManager::tryToKill()
{
pimpl_->tryToKill();
}
#include "moc_instancemanager.cpp"
#include "instancemanager.moc"

View file

@ -15,45 +15,27 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// Based on: https://stackoverflow.com/a/28172162
#pragma once
#include <QObject>
#include <QSharedMemory>
#include <QSystemSemaphore>
#include <QtNetwork/QLocalServer>
#include <QtNetwork/QLocalSocket>
#include <memory>
class MainApplication;
class RunGuard : public QObject
class InstanceManager final : public QObject
{
Q_OBJECT;
Q_DISABLE_COPY(InstanceManager)
public:
RunGuard(const QString& key, MainApplication* mainApp = nullptr);
~RunGuard();
explicit InstanceManager(MainApplication* mainApp);
~InstanceManager();
bool isAnotherRunning();
bool tryToRun();
void release();
private Q_SLOTS:
void tryRestorePrimaryInstance();
void tryToKill();
private:
MainApplication* mainAppInstance_;
const QString key_;
const QString memLockKey_;
const QString sharedmemKey_;
QSharedMemory sharedMem_;
QSystemSemaphore memLock_;
QLocalSocket* socket_ {nullptr};
QLocalServer* server_ {nullptr};
Q_DISABLE_COPY(RunGuard)
class Impl;
std::unique_ptr<Impl> pimpl_;
};

View file

@ -1,4 +1,4 @@
/*!
/*
* Copyright (C) 2015-2022 Savoir-faire Linux Inc.
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@ -19,7 +19,7 @@
*/
#include "mainapplication.h"
#include "runguard.h"
#include "instancemanager.h"
#include "version.h"
#include <QCryptographicHash>
@ -97,29 +97,24 @@ main(int argc, char* argv[])
MainApplication app(argc, newArgv);
/*
* Runguard to make sure that only one instance runs at a time.
* Note: needs to be after the creation of the application
*/
QCryptographicHash appData(QCryptographicHash::Sha256);
appData.addData(QApplication::applicationName().toUtf8());
appData.addData(QApplication::organizationDomain().toUtf8());
RunGuard guard(appData.result(), &app);
if (!guard.tryToRun()) {
// InstanceManager prevents multiple instances, and will handle
// IPC termination requests to and from secondary instances, which
// is used to gracefully terminate the app from an installer script
// during an update.
InstanceManager im(&app);
if (app.getOpt(MainApplication::Option::TerminationRequested).toBool()) {
qWarning() << "Attempting to terminate other instances.";
im.tryToKill();
return 0;
} else if (!im.tryToRun()) {
qWarning() << "Another instance is running.";
return 0;
}
if (!app.init()) {
guard.release();
return 0;
}
/*
* Exec the application.
*/
auto ret = app.exec();
guard.release();
return ret;
return app.exec();
}
#endif

View file

@ -53,16 +53,6 @@
#include <gnutls/gnutls.h>
#endif
namespace opts {
// Keys used to store command-line options.
constexpr static const char STARTMINIMIZED[] = "STARTMINIMIZED";
constexpr static const char DEBUG[] = "DEBUG";
constexpr static const char DEBUGCONSOLE[] = "DEBUGCONSOLE";
constexpr static const char DEBUGFILE[] = "DEBUGFILE";
constexpr static const char UPDATEURL[] = "UPDATEURL";
constexpr static const char MUTEDAEMON[] = "MUTEDAEMON";
} // namespace opts
static void
consoleDebug()
{
@ -147,6 +137,7 @@ MainApplication::fileDebug(QFile* debugFile)
MainApplication::MainApplication(int& argc, char** argv)
: QApplication(argc, argv)
{
parseArguments();
QObject::connect(this, &QApplication::aboutToQuit, [this] { cleanup(); });
}
@ -179,9 +170,7 @@ MainApplication::init()
setenv("QT_QPA_PLATFORMTHEME", "gtk3", true);
#endif
auto results = parseArguments();
if (results[opts::DEBUG].toBool()) {
if (runOptions_[Option::Debug].toBool()) {
consoleDebug();
}
@ -193,9 +182,9 @@ MainApplication::init()
gnutls_global_init();
#endif
initLrc(results[opts::UPDATEURL].toString(),
initLrc(runOptions_[Option::UpdateUrl].toString(),
connectivityMonitor_.get(),
results[opts::DEBUG].toBool() && !results[opts::MUTEDAEMON].toBool());
runOptions_[Option::Debug].toBool() && !runOptions_[Option::MuteJamid].toBool());
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
using namespace Interfaces;
@ -206,10 +195,12 @@ MainApplication::init()
engine_->load(QUrl(QStringLiteral("qrc:/src/DaemonReconnectWindow.qml")));
exec();
if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid()))
if ((!lrc::api::Lrc::isConnected()) || (!lrc::api::Lrc::dbusIsValid())) {
qWarning() << "Can't connect to the daemon via D-Bus.";
return false;
else
} else {
engine_.reset(new QQmlApplicationEngine());
}
}
#endif
@ -228,14 +219,14 @@ MainApplication::init()
[this] { engine_->quit(); },
Qt::DirectConnection);
if (results[opts::DEBUGFILE].toBool()) {
if (runOptions_[Option::DebugToFile].toBool()) {
debugFile_.reset(new QFile(getDebugFilePath()));
debugFile_->open(QIODevice::WriteOnly | QIODevice::Truncate);
debugFile_->close();
fileDebug(debugFile_.get());
}
if (results[opts::DEBUGCONSOLE].toBool()) {
if (runOptions_[Option::DebugToConsole].toBool()) {
vsConsoleDebug();
}
@ -253,7 +244,7 @@ MainApplication::init()
initQmlLayer();
settingsManager_->setValue(Settings::Key::StartMinimized,
results[opts::STARTMINIMIZED].toBool());
runOptions_[Option::StartMinimized].toBool());
initSystray();
@ -296,10 +287,9 @@ MainApplication::initLrc(const QString& downloadUrl, ConnectivityMonitor* cm, bo
lrcInstance_->subscribeToDebugReceived();
}
const QVariantMap
void
MainApplication::parseArguments()
{
QVariantMap results;
QCommandLineParser parser;
parser.addHelpOption();
parser.addVersionOption();
@ -325,32 +315,34 @@ MainApplication::parseArguments()
QCommandLineOption debugOption({"d", "debug"}, "Debug out.");
parser.addOption(debugOption);
QCommandLineOption debugFileOption({"f", "file"}, "Debug to file.");
parser.addOption(debugFileOption);
#ifdef Q_OS_WINDOWS
QCommandLineOption debugConsoleOption({"c", "console"}, "Debug out to IDE console.");
parser.addOption(debugConsoleOption);
QCommandLineOption debugFileOption({"f", "file"}, "Debug to file.");
parser.addOption(debugFileOption);
QCommandLineOption updateUrlOption({"u", "url"}, "<url> for debugging version queries.", "url");
parser.addOption(updateUrlOption);
#endif
QCommandLineOption terminateOption({"t", "term"}, "Terminate all instances.");
parser.addOption(terminateOption);
QCommandLineOption muteDaemonOption({"q", "quiet"}, "Mute daemon logging. (only if debug)");
parser.addOption(muteDaemonOption);
parser.process(*this);
results[opts::STARTMINIMIZED] = parser.isSet(minimizedOption);
results[opts::DEBUG] = parser.isSet(debugOption);
runOptions_[Option::StartMinimized] = parser.isSet(minimizedOption);
runOptions_[Option::Debug] = parser.isSet(debugOption);
runOptions_[Option::DebugToFile] = parser.isSet(debugFileOption);
#ifdef Q_OS_WINDOWS
results[opts::DEBUGCONSOLE] = parser.isSet(debugConsoleOption);
results[opts::DEBUGFILE] = parser.isSet(debugFileOption);
results[opts::UPDATEURL] = parser.value(updateUrlOption);
runOptions_[Option::DebugToConsole] = parser.isSet(debugConsoleOption);
runOptions_[Option::UpdateUrl] = parser.value(updateUrlOption);
#endif
results[opts::MUTEDAEMON] = parser.isSet(muteDaemonOption);
return results;
runOptions_[Option::TerminationRequested] = parser.isSet(terminateOption);
runOptions_[Option::MuteJamid] = parser.isSet(muteDaemonOption);
}
void

View file

@ -57,6 +57,7 @@ private:
class MainApplication : public QApplication
{
Q_OBJECT
Q_DISABLE_COPY(MainApplication)
public:
explicit MainApplication(int& argc, char** argv);
@ -65,21 +66,36 @@ public:
bool init();
void restoreApp();
enum class Option {
StartMinimized = 0,
Debug,
DebugToConsole,
DebugToFile,
UpdateUrl,
MuteJamid,
TerminationRequested
};
QVariant getOpt(const Option opt)
{
return runOptions_[opt];
};
Q_SIGNALS:
void closeRequested();
private:
void vsConsoleDebug();
void fileDebug(QFile* debugFile);
void initLrc(const QString& downloadUrl, ConnectivityMonitor* cm, bool logDaemon);
const QVariantMap parseArguments();
void parseArguments();
void setApplicationFont();
void initQmlLayer();
void initSystray();
void cleanup();
private:
std::map<Option, QVariant> runOptions_;
QScopedPointer<QFile> debugFile_;
QScopedPointer<QQmlApplicationEngine> engine_;
QScopedPointer<LRCInstance> lrcInstance_;

View file

@ -1,136 +0,0 @@
/*
* Copyright (C) 2019-2022 Savoir-faire Linux Inc.
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Based on: https://stackoverflow.com/a/28172162
#include "runguard.h"
#include "mainapplication.h"
#include <QCryptographicHash>
#include <QLocalSocket>
namespace {
QString
generateKeyHash(const QString& key, const QString& salt)
{
QByteArray data;
data.append(key.toUtf8());
data.append(salt.toUtf8());
data = QCryptographicHash::hash(data, QCryptographicHash::Sha1).toHex();
return data;
}
} // namespace
RunGuard::RunGuard(const QString& key, MainApplication* mainApp)
: key_(key)
, memLockKey_(generateKeyHash(key, "_memLockKey"))
, sharedmemKey_(generateKeyHash(key, "_sharedmemKey"))
, sharedMem_(sharedmemKey_)
, memLock_(memLockKey_, 1)
, mainAppInstance_(mainApp)
{}
RunGuard::~RunGuard()
{
release();
}
void
RunGuard::tryRestorePrimaryInstance()
{
mainAppInstance_->restoreApp();
}
bool
RunGuard::isAnotherRunning()
{
if (sharedMem_.isAttached())
return false;
memLock_.acquire();
const bool isRunning = sharedMem_.attach();
if (isRunning)
sharedMem_.detach();
memLock_.release();
return isRunning;
}
bool
RunGuard::tryToRun()
{
if (isAnotherRunning()) {
/*
* This is a secondary instance,
* connect to the primary instance to trigger a restore
* then fail.
*/
if (!socket_)
socket_ = new QLocalSocket();
if (!socket_)
return false;
if (socket_->state() == QLocalSocket::UnconnectedState
|| socket_->state() == QLocalSocket::ClosingState) {
socket_->connectToServer(key_);
}
if (socket_->state() == QLocalSocket::ConnectingState) {
socket_->waitForConnected();
}
if (socket_->state() == QLocalSocket::ConnectedState) {
return false;
}
// If not connected, this means that the server doesn't exists
// and the app can be relaunched (can be the case after a client crash or Ctrl+C)
}
memLock_.acquire();
const bool result = sharedMem_.create(sizeof(quint64));
memLock_.release();
if (!result) {
release();
return false;
}
/*
* This is the primary instance,
* listen for subsequent instances.
*/
QLocalServer::removeServer(key_);
server_ = new QLocalServer();
server_->setSocketOptions(QLocalServer::UserAccessOption);
server_->listen(key_);
QObject::connect(server_,
&QLocalServer::newConnection,
this,
&RunGuard::tryRestorePrimaryInstance);
return true;
}
void
RunGuard::release()
{
memLock_.acquire();
if (sharedMem_.isAttached())
sharedMem_.detach();
memLock_.release();
}