From dcdd5996223a4e1043cd4e492f976fd432de0691 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Sat, 7 Jun 2025 17:26:49 -0700 Subject: [PATCH 1/2] Factor out common socket messaging code to simplify adding new single instance commands --- src/gui/Application.cpp | 71 ++++++++++++++++------------------------- src/gui/Application.h | 5 +++ 2 files changed, 33 insertions(+), 43 deletions(-) diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 01553ad91..402b50c3d 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -47,6 +47,12 @@ namespace int g_OriginalFontSize = 0; } // namespace +enum Application::SocketCmd : quint32 +{ + OpenFiles = 1, + LockAll, +}; + Application::Application(int& argc, char** argv) : QApplication(argc, argv) #ifdef Q_OS_UNIX @@ -54,11 +60,7 @@ Application::Application(int& argc, char** argv) #endif , m_alreadyRunning(false) , m_lockFile(nullptr) -#if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)) { -#else -{ -#endif #if defined(Q_OS_UNIX) registerUnixSignals(); #endif @@ -324,13 +326,13 @@ void Application::socketReadyRead() return; } - QStringList fileNames; - quint32 id; - in >> id; + SocketCmd id; + // manual reinterpret_cast not needed for Qt 5.14+ + in >> reinterpret_cast::type&>(id); - // TODO: move constants to enum switch (id) { - case 1: + case SocketCmd::OpenFiles: { + QStringList fileNames; in >> fileNames; for (const QString& fileName : asConst(fileNames)) { const QFileInfo fInfo(fileName); @@ -338,9 +340,9 @@ void Application::socketReadyRead() emit openFile(fileName); } } - break; - case 2: + } + case SocketCmd::LockAll: getMainWindow()->lockAllDatabases(); break; } @@ -357,13 +359,7 @@ bool Application::isAlreadyRunning() const return config()->get(Config::SingleInstance).toBool() && m_alreadyRunning; } -/** - * Send to-open file names to the running UI instance - * - * @param fileNames - list of file names to open - * @return true if all operations succeeded (connection made, data sent, connection closed) - */ -bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) +bool Application::sendSocketCommand(SocketCmd id, const std::function& caller) { QLocalSocket client; client.connectToServer(m_socketName); @@ -376,8 +372,8 @@ bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) QDataStream out(&data, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_5_0); out << quint32(0); // reserve space for block size - out << quint32(1); // ID for file name send. TODO: move to enum - out << fileNames; // send file names to be opened + out << id; // ID of command being sent. + caller(out); // Pass to caller to add any additional data out.device()->seek(0); out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size @@ -388,6 +384,17 @@ bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) return writeOk && disconnected; } +/** + * Send to-open file names to the running UI instance + * + * @param fileNames - list of file names to open + * @return true if all operations succeeded (connection made, data sent, connection closed) + */ +bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) +{ + return this->sendSocketCommand(SocketCmd::OpenFiles, [fileNames](QDataStream& out) { out << fileNames; }); +} + /** * Locks all open databases in the running instance * @@ -395,29 +402,7 @@ bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames) */ bool Application::sendLockToInstance() { - // Make a connection to avoid SIGSEGV - QLocalSocket client; - client.connectToServer(m_socketName); - const bool connected = client.waitForConnected(WaitTimeoutMSec); - if (!connected) { - return false; - } - - // Send lock signal - QByteArray data; - QDataStream out(&data, QIODevice::WriteOnly); - out.setVersion(QDataStream::Qt_5_0); - out << quint32(0); // reserve space for block size - out << quint32(2); // ID for database lock. TODO: move to enum - out.device()->seek(0); - out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size - - // Finish gracefully - const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec); - client.disconnectFromServer(); - const bool disconnected = - client.state() == QLocalSocket::UnconnectedState || client.waitForConnected(WaitTimeoutMSec); - return writeOk && disconnected; + return this->sendSocketCommand(SocketCmd::LockAll, [](QDataStream&) { /* No Data */ }); } bool Application::isDarkTheme() const diff --git a/src/gui/Application.h b/src/gui/Application.h index 349c93923..4d7a87fca 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -23,6 +23,7 @@ #include #include #include +#include #if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)) @@ -78,6 +79,10 @@ private: static void handleUnixSignal(int sig); static int unixSignalSocket[2]; #endif + + enum SocketCmd : quint32; + bool sendSocketCommand(SocketCmd id, const std::function&); + bool m_alreadyRunning; bool m_darkTheme = false; QLockFile* m_lockFile; From 4132a7efac65876f249a0fc94bca99764795fab3 Mon Sep 17 00:00:00 2001 From: "Michael Ziminsky (Z)" Date: Sat, 7 Jun 2025 21:18:25 -0700 Subject: [PATCH 2/2] Support full DB unlock in single instance mode Closes #2089 --- src/gui/Application.cpp | 26 +++++++++++++++++++------- src/gui/Application.h | 3 ++- src/gui/MainWindow.cpp | 2 +- src/main.cpp | 38 +++++++++++++++++++++++++------------- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index 402b50c3d..969f623d7 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -51,6 +51,7 @@ enum Application::SocketCmd : quint32 { OpenFiles = 1, LockAll, + Unlock, }; Application::Application(int& argc, char** argv) @@ -327,8 +328,7 @@ void Application::socketReadyRead() } SocketCmd id; - // manual reinterpret_cast not needed for Qt 5.14+ - in >> reinterpret_cast::type&>(id); + in >> id; switch (id) { case SocketCmd::OpenFiles: { @@ -345,6 +345,11 @@ void Application::socketReadyRead() case SocketCmd::LockAll: getMainWindow()->lockAllDatabases(); break; + case SocketCmd::Unlock: + QString filename, password, keyfile; + in >> filename >> password >> keyfile; + emit openFile(filename, password, keyfile); + break; } socket->deleteLater(); @@ -352,10 +357,6 @@ void Application::socketReadyRead() bool Application::isAlreadyRunning() const { -#ifdef QT_DEBUG - // In DEBUG mode we can run unlimited instances - return false; -#endif return config()->get(Config::SingleInstance).toBool() && m_alreadyRunning; } @@ -392,7 +393,7 @@ bool Application::sendSocketCommand(SocketCmd id, const std::functionsendSocketCommand(SocketCmd::OpenFiles, [fileNames](QDataStream& out) { out << fileNames; }); + return this->sendSocketCommand(SocketCmd::OpenFiles, [&](QDataStream& out) { out << fileNames; }); } /** @@ -405,6 +406,17 @@ bool Application::sendLockToInstance() return this->sendSocketCommand(SocketCmd::LockAll, [](QDataStream&) { /* No Data */ }); } +/** + * Open and unlock a database file in the running instance + * + * @return true if the instance receives the request + */ +bool Application::sendUnlockToInstance(const QString& filename, const QString& password, const QString& keyfile) +{ + return this->sendSocketCommand(SocketCmd::Unlock, + [&](QDataStream& out) { out << filename << password << keyfile; }); +} + bool Application::isDarkTheme() const { return m_darkTheme; diff --git a/src/gui/Application.h b/src/gui/Application.h index 4d7a87fca..b00f8ce99 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -53,11 +53,12 @@ public: bool sendFileNamesToRunningInstance(const QStringList& fileNames); bool sendLockToInstance(); + bool sendUnlockToInstance(const QString& filename, const QString& password = {}, const QString& keyfile = {}); void restart(); signals: - void openFile(const QString& filename); + void openFile(const QString& filename, const QString& password = {}, const QString& keyfile = {}); void anotherInstanceStarted(); void applicationActivated(); void quitSignalReceived(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index a051187d6..067f7250c 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -661,8 +661,8 @@ MainWindow::MainWindow() connect(qApp, SIGNAL(anotherInstanceStarted()), this, SLOT(bringToFront())); connect(qApp, SIGNAL(applicationActivated()), this, SLOT(bringToFront())); - connect(qApp, SIGNAL(openFile(QString)), this, SLOT(openDatabase(QString))); connect(qApp, SIGNAL(quitSignalReceived()), this, SLOT(appExit()), Qt::DirectConnection); + connect(static_cast(qApp), &Application::openFile, this, &MainWindow::openDatabase); // Setup the status bar statusBar()->setFixedHeight(24); diff --git a/src/main.cpp b/src/main.cpp index 2c4da4c1f..f9af22ecc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -49,6 +49,18 @@ Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) #include #endif +namespace +{ + QString promptPassword() + { + // we always need consume a line of STDIN if --pw-stdin is set to clear out the + // buffer for native messaging, even if the specified file does not exist + QTextStream out(stdout, QIODevice::WriteOnly); + out << QObject::tr("Database password: ") << Qt::flush; + return Utils::getPassword(); + } +} // namespace + int main(int argc, char** argv) { QT_REQUIRE_VERSION(argc, argv, QT_VERSION_STR) @@ -140,9 +152,14 @@ int main(int argc, char** argv) } } #endif + Utils::setDefaultTextStreams(); + + const bool pwstdin = parser.isSet(pwstdinOption); + const QString keyfile = parser.value(keyfileOption); // Process single instance and early exit if already running if (app.isAlreadyRunning()) { + qWarning() << QObject::tr("Another instance of KeePassXC is already running.").toUtf8().constData(); if (parser.isSet(lockOption)) { if (app.sendLockToInstance()) { qInfo() << QObject::tr("Databases have been locked.").toUtf8().constData(); @@ -151,11 +168,13 @@ int main(int argc, char** argv) return EXIT_FAILURE; } } else { - if (!fileNames.isEmpty()) { - app.sendFileNamesToRunningInstance(fileNames); + for (const QString& filename : fileNames) { + QString password; + if (pwstdin) { + password = promptPassword(); + } + app.sendUnlockToInstance(filename, password, keyfile); } - - qWarning() << QObject::tr("Another instance of KeePassXC is already running.").toUtf8().constData(); } return EXIT_SUCCESS; } @@ -176,8 +195,6 @@ int main(int argc, char** argv) return EXIT_FAILURE; } - Utils::setDefaultTextStreams(); - // Apply the configured theme before creating any GUI elements app.applyTheme(); @@ -195,17 +212,12 @@ int main(int argc, char** argv) // This ensures any top-level windows (Main Window, Modal Dialogs, etc.) are excluded from screenshots mainWindow.setAllowScreenCapture(parser.isSet(allowScreenCaptureOption)); - const bool pwstdin = parser.isSet(pwstdinOption); for (const QString& filename : fileNames) { QString password; if (pwstdin) { - // we always need consume a line of STDIN if --pw-stdin is set to clear out the - // buffer for native messaging, even if the specified file does not exist - QTextStream out(stdout, QIODevice::WriteOnly); - out << QObject::tr("Database password: ") << Qt::flush; - password = Utils::getPassword(); + password = promptPassword(); } - mainWindow.openDatabase(filename, password, parser.value(keyfileOption)); + mainWindow.openDatabase(filename, password, keyfile); } // start minimized if configured