Add support for running in single instance mode on Linux and Windows

Prevent reopening already opened repo on IPC call
This commit is contained in:
kas 2021-12-11 13:08:57 +01:00
parent 5ebdca8879
commit 193e1f3b0a
6 changed files with 223 additions and 2 deletions

View File

@ -49,7 +49,7 @@ set(QT_MODULES
Test
)
if(APPLE)
if(UNIX)
set(QT_MODULES ${QT_MODULES} DBus)
endif()

View File

@ -14,6 +14,7 @@
#include "ui/MainWindow.h"
#include "ui/MenuBar.h"
#include "ui/RepoView.h"
#include "ui/TabWidget.h"
#include "update/Updater.h"
#include <QCloseEvent>
#include <QCommandLineParser>
@ -33,13 +34,17 @@
#include <QUrlQuery>
#include <QUuid>
#if defined(Q_OS_MAC)
#if defined(Q_OS_LINUX)
#include <QtDBus/QtDBus>
#elif defined(Q_OS_MAC)
#include <unistd.h>
#elif defined(Q_OS_WIN)
#include <windows.h>
#include <dbghelp.h>
#include <strsafe.h>
#include <QWindow>
static LPTOP_LEVEL_EXCEPTION_FILTER defaultFilter = nullptr;
@ -274,6 +279,178 @@ bool Application::restoreWindows()
return MainWindow::restoreWindows();
}
static MainWindow *openOrSwitch(QDir repo)
{
repo.makeAbsolute();
QList<MainWindow *> windows = MainWindow::windows();
for (MainWindow *window : windows) {
TabWidget *tabs = window->tabWidget();
for (int i = 0; i < tabs->count(); ++i) {
RepoView *view = (RepoView *)tabs->widget(i);
QDir openRepo = QDir(view->repo().workdir().path());
if (openRepo == repo) {
tabs->setCurrentIndex(i);
return window;
}
}
}
return MainWindow::open(repo.path(), true);
}
#if defined(Q_OS_LINUX)
#define DBUS_SERVICE_NAME "com.github.Murmele.Gittyup"
#define DBUS_INTERFACE_NAME "com.github.Murmele.Gittyup.Application"
#define DBUS_OBJECT_PATH "/com/github/Murmele/Gittyup/Application"
DBusGittyup::DBusGittyup(QObject *parent): QObject(parent)
{
}
void DBusGittyup::openRepository(const QString &repo)
{
openOrSwitch(QDir(repo));
}
void DBusGittyup::openAndFocusRepository(const QString &repo)
{
openOrSwitch(QDir(repo))->activateWindow();
}
void DBusGittyup::setFocus()
{
MainWindow::activeWindow()->activateWindow();
}
#elif defined(Q_OS_WIN)
#define COPYDATA_WINDOW_TITLE "Gittyup WM_COPYDATA receiver 16b8b3f6-6446-4fa7-8c72-53c25b1f206c"
enum CopyDataCommand
{
Focus = 0,
FocusAndOpen = 1
};
namespace
{
// Helper window class for receiving IPC messages
class CopyDataWindow: public QWindow
{
public:
CopyDataWindow()
{
setTitle(COPYDATA_WINDOW_TITLE);
}
protected:
virtual bool nativeEvent(const QByteArray &eventType, void *message, long *result) Q_DECL_OVERRIDE
{
MSG *msg = (MSG*) message;
if (msg->message == WM_COPYDATA) {
COPYDATASTRUCT *cds = (COPYDATASTRUCT*) msg->lParam;
switch (cds->dwData) {
case CopyDataCommand::Focus:
MainWindow::activeWindow()->activateWindow();
break;
case CopyDataCommand::FocusAndOpen:
if (cds->cbData % 2 == 0) {
QString repo = QString::fromUtf16((const char16_t*) cds->lpData, cds->cbData / 2);
openOrSwitch(QDir(repo));
MainWindow::activeWindow()->activateWindow();
}
break;
}
return true;
}
return QWindow::nativeEvent(eventType, message, result);
}
};
}
#endif
bool Application::runSingleInstance()
{
if (Settings::instance()->value("singleInstance").toBool()) {
#if defined(Q_OS_LINUX)
QDBusConnection bus = QDBusConnection::sessionBus();
if (bus.isConnected()) {
QDBusInterface masterInstance(DBUS_SERVICE_NAME, DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, bus);
// Is another instance running on the current DBus session bus?
if (masterInstance.isValid()) {
if (!mPositionalArguments.isEmpty())
masterInstance.call("openAndFocusRepository", QDir(mPositionalArguments.first()).absolutePath());
return true;
}
}
#elif defined(Q_OS_WIN)
HWND handle = FindWindowA(nullptr, COPYDATA_WINDOW_TITLE);
// Is another instance running in the current session?
if (handle != nullptr) {
QWindow sender;
COPYDATASTRUCT cds;
if (mPositionalArguments.isEmpty()) {
cds.dwData = CopyDataCommand::Focus;
cds.cbData = 0;
cds.lpData = nullptr;
SendMessage(handle, WM_COPYDATA, sender.winId(), (LPARAM) &cds);
} else {
QString arg = QDir(mPositionalArguments.first()).absolutePath();
cds.dwData = CopyDataCommand::FocusAndOpen;
cds.cbData = arg.length() * 2;
cds.lpData = (LPVOID) arg.utf16();
SendMessage(handle, WM_COPYDATA, sender.winId(), (LPARAM) &cds);
}
return true;
}
#endif
}
#ifndef Q_OS_MAC
registerService();
#endif
return false;
}
#ifndef Q_OS_MAC
void Application::registerService()
{
#if defined(Q_OS_LINUX)
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected())
return;
if (!bus.registerService(DBUS_SERVICE_NAME))
return;
bus.registerObject(DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, new DBusGittyup(), QDBusConnection::ExportScriptableSlots);
#elif defined(Q_OS_WIN)
CopyDataWindow *receiver = new CopyDataWindow();
receiver->winId();
#endif
}
#endif
Theme *Application::theme()
{
return static_cast<Application *>(instance())->mTheme.data();

View File

@ -27,6 +27,7 @@ public:
void autoUpdate();
bool restoreWindows();
bool runSingleInstance();
static Theme *theme();
@ -42,4 +43,19 @@ private:
QStringList mPositionalArguments;
};
#ifdef Q_OS_LINUX
class DBusGitAhead: public QObject
{
Q_OBJECT
public:
DBusGitAhead(QObject *parent = nullptr);
public slots:
Q_SCRIPTABLE void openRepository(const QString &repo);
Q_SCRIPTABLE void openAndFocusRepository(const QString &repo);
Q_SCRIPTABLE void setFocus();
};
#endif
#endif

View File

@ -52,6 +52,10 @@ add_library(app
${THEME_IMPL_FILE}
)
if(UNIX AND NOT APPLE)
set_target_properties(app PROPERTIES AUTOMOC ON)
endif()
target_compile_definitions(app
PUBLIC
GITTYUP_NAME="${GITTYUP_NAME}"
@ -72,6 +76,12 @@ set_target_properties(app PROPERTIES
AUTOMOC ON
)
if(UNIX AND NOT APPLE)
target_link_libraries(app
Qt5::DBus
)
endif()
# Add main executable.
add_executable(gittyup WIN32 MACOSX_BUNDLE
Gittyup.cpp

View File

@ -16,6 +16,10 @@ int main(int argc, char *argv[])
Application::setAttribute(Qt::AA_UseHighDpiPixmaps);
Application app(argc, argv, true);
// Check if only one running instance is allowed and already running
if (app.runSingleInstance())
return 0;
// Restore windows before checking for updates so that
// the update dialog pops up on top of the other windows.
if (!app.restoreWindows())

View File

@ -125,6 +125,9 @@ public:
AboutDialog::openSharedInstance(AboutDialog::Privacy);
});
mSingleInstance = new QCheckBox(
tr("Only allow a single running instance"), this);
QFormLayout *form = new QFormLayout;
form->addRow(tr("User name:"), mName);
form->addRow(tr("User email:"), mEmail);
@ -136,6 +139,10 @@ public:
form->addRow(tr("Credentials:"), mStoreCredentials);
form->addRow(QString(), privacy);
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
form->addRow(tr("Single instance:"), mSingleInstance);
#endif
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(16,12,16,12);
layout->addLayout(form);
@ -186,6 +193,10 @@ public:
Settings::instance()->setValue("credential/store", checked);
delete CredentialHelper::instance();
});
connect(mSingleInstance, &QCheckBox::toggled, [](bool checked) {
Settings::instance()->setValue("singleInstance", checked);
});
}
void init()
@ -208,6 +219,8 @@ public:
mNoTranslation->setChecked(settings->value("translation/disable").toBool());
mStoreCredentials->setChecked(settings->value("credential/store").toBool());
mSingleInstance->setChecked(settings->value("singleInstance").toBool());
}
private:
@ -221,6 +234,7 @@ private:
QCheckBox *mAutoPrune;
QCheckBox *mNoTranslation;
QCheckBox *mStoreCredentials;
QCheckBox *mSingleInstance;
};
class ToolsPanel : public QWidget