Merge pull request #1258 from frostasm/pass-command-line-filenames-to-already-running-instance

Pass command line filenames to already running instance
This commit is contained in:
Janek Bevendorff 2018-01-03 16:03:17 +01:00 committed by GitHub
commit ffddcb5060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 218 additions and 57 deletions

View File

@ -22,13 +22,15 @@
#include "core/Config.h"
#include <QAbstractNativeEventFilter>
#include <QFileInfo>
#include <QFileOpenEvent>
#include <QSocketNotifier>
#include <QLockFile>
#include <QSocketNotifier>
#include <QStandardPaths>
#include <QtNetwork/QLocalSocket>
#include "autotype/AutoType.h"
#include "core/Global.h"
#if defined(Q_OS_UNIX)
#include <signal.h>
@ -36,6 +38,11 @@
#include <sys/socket.h>
#endif
namespace {
constexpr int WaitTimeoutMSec = 150;
const char BlockSizeProperty[] = "blockSize";
}
#if defined(Q_OS_UNIX) && !defined(Q_OS_OSX)
class XcbEventFilter : public QAbstractNativeEventFilter
{
@ -105,8 +112,8 @@ Application::Application(int& argc, char** argv)
// In DEBUG mode don't interfere with Release instances
identifier += "-DEBUG";
#endif
QString socketName = identifier + ".socket";
QString lockName = identifier + ".lock";
m_socketName = identifier + ".socket";
// According to documentation we should use RuntimeLocation on *nixes, but even Qt doesn't respect
// this and creates sockets in TempLocation, so let's be consistent.
@ -114,20 +121,22 @@ Application::Application(int& argc, char** argv)
m_lockFile->setStaleLockTime(0);
m_lockFile->tryLock();
m_lockServer.setSocketOptions(QLocalServer::UserAccessOption);
connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted()));
connect(&m_lockServer, SIGNAL(newConnection()), this, SLOT(processIncomingConnection()));
switch (m_lockFile->error()) {
case QLockFile::NoError:
// No existing lock was found, start listener
m_lockServer.setSocketOptions(QLocalServer::UserAccessOption);
m_lockServer.listen(socketName);
connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted()));
m_lockServer.listen(m_socketName);
break;
case QLockFile::LockFailedError: {
if (config()->get("SingleInstance").toBool()) {
// Attempt to connect to the existing instance
QLocalSocket client;
for (int i = 0; i < 3; i++) {
client.connectToServer(socketName);
if (client.waitForConnected(150)) {
for (int i = 0; i < 3; ++i) {
client.connectToServer(m_socketName);
if (client.waitForConnected(WaitTimeoutMSec)) {
// Connection succeeded, this will raise the existing window if minimized
client.abort();
m_alreadyRunning = true;
@ -145,9 +154,7 @@ Application::Application(int& argc, char** argv)
m_lockFile->removeStaleLockFile();
m_lockFile->tryLock();
// start the listen server
m_lockServer.setSocketOptions(QLocalServer::UserAccessOption);
m_lockServer.listen(socketName);
connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted()));
m_lockServer.listen(m_socketName);
}
}
break;
@ -187,12 +194,8 @@ bool Application::event(QEvent* event)
}
#ifdef Q_OS_MAC
// restore main window when clicking on the docker icon
else if ((event->type() == QEvent::ApplicationActivate) && m_mainWindow) {
m_mainWindow->ensurePolished();
m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized);
m_mainWindow->show();
m_mainWindow->raise();
m_mainWindow->activateWindow();
else if (event->type() == QEvent::ApplicationActivate) {
emit applicationActivated();
}
#endif
@ -247,12 +250,54 @@ void Application::quitBySignal()
m_unixSignalNotifier->setEnabled(false);
char buf;
Q_UNUSED(::read(unixSignalSocket[1], &buf, sizeof(buf)));
if (nullptr != m_mainWindow)
static_cast<MainWindow*>(m_mainWindow)->appExit();
emit quitSignalReceived();
}
#endif
void Application::processIncomingConnection()
{
if (m_lockServer.hasPendingConnections()) {
QLocalSocket* socket = m_lockServer.nextPendingConnection();
socket->setProperty(BlockSizeProperty, 0);
connect(socket, SIGNAL(readyRead()), this, SLOT(socketReadyRead()));
}
}
void Application::socketReadyRead()
{
QLocalSocket* socket = qobject_cast<QLocalSocket*>(sender());
if (!socket) {
return;
}
QDataStream in(socket);
in.setVersion(QDataStream::Qt_5_0);
int blockSize = socket->property(BlockSizeProperty).toInt();
if (blockSize == 0) {
// Relies on the fact that QDataStream format streams a quint32 into sizeof(quint32) bytes
if (socket->bytesAvailable() < qint64(sizeof(quint32))) {
return;
}
in >> blockSize;
}
if (socket->bytesAvailable() < blockSize || in.atEnd()) {
socket->setProperty(BlockSizeProperty, blockSize);
return;
}
QStringList fileNames;
in >> fileNames;
for (const QString& fileName: asConst(fileNames)) {
const QFileInfo fInfo(fileName);
if (fInfo.isFile() && fInfo.suffix().toLower() == "kdbx") {
emit openFile(fileName);
}
}
socket->deleteLater();
}
bool Application::isAlreadyRunning() const
{
#ifdef QT_DEBUG
@ -262,3 +307,26 @@ bool Application::isAlreadyRunning() const
return config()->get("SingleInstance").toBool() && m_alreadyRunning;
}
bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames)
{
QLocalSocket client;
client.connectToServer(m_socketName);
const bool connected = client.waitForConnected(WaitTimeoutMSec);
if (!connected) {
return false;
}
QByteArray data;
QDataStream out(&data, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_5_0);
out << quint32(0)
<< fileNames;
out.device()->seek(0);
out << quint32(data.size() - sizeof(quint32));
const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec);
client.disconnectFromServer();
const bool disconnected = client.waitForDisconnected(WaitTimeoutMSec);
return writeOk && disconnected;
}

View File

@ -39,14 +39,20 @@ public:
bool event(QEvent* event) override;
bool isAlreadyRunning() const;
bool sendFileNamesToRunningInstance(const QStringList& fileNames);
signals:
void openFile(const QString& filename);
void anotherInstanceStarted();
void applicationActivated();
void quitSignalReceived();
private slots:
#if defined(Q_OS_UNIX)
void quitBySignal();
#endif
void processIncomingConnection();
void socketReadyRead();
private:
QWidget* m_mainWindow;
@ -63,6 +69,7 @@ private:
bool m_alreadyRunning;
QLockFile* m_lockFile;
QLocalServer m_lockServer;
QString m_socketName;
};
#endif // KEEPASSX_APPLICATION_H

View File

@ -114,6 +114,8 @@ MainWindow::MainWindow()
{
m_ui->setupUi(this);
setAcceptDrops(true);
m_ui->toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
// Setup the search widget in the toolbar
@ -879,11 +881,7 @@ void MainWindow::toggleWindow()
if ((QApplication::activeWindow() == this) && isVisible() && !isMinimized()) {
hideWindow();
} else {
ensurePolished();
setWindowState(windowState() & ~Qt::WindowMinimized);
show();
raise();
activateWindow();
bringToFront();
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(QT_NO_DBUS) && (QT_VERSION < QT_VERSION_CHECK(5, 9, 0))
// re-register global D-Bus menu (needed on Ubuntu with Unity)
@ -991,9 +989,57 @@ void MainWindow::hideYubiKeyPopup()
setEnabled(true);
}
void MainWindow::bringToFront()
{
ensurePolished();
setWindowState(windowState() & ~Qt::WindowMinimized);
show();
raise();
activateWindow();
}
void MainWindow::handleScreenLock()
{
if (config()->get("security/lockdatabasescreenlock").toBool()){
lockDatabasesAfterInactivity();
}
}
QStringList MainWindow::kdbxFilesFromUrls(const QList<QUrl>& urls)
{
QStringList kdbxFiles;
for (const QUrl& url: urls) {
const QFileInfo fInfo(url.toLocalFile());
const bool isKdbxFile = fInfo.isFile() && fInfo.suffix().toLower() == "kdbx";
if (isKdbxFile) {
kdbxFiles.append(fInfo.absoluteFilePath());
}
}
return kdbxFiles;
}
void MainWindow::dragEnterEvent(QDragEnterEvent* event)
{
const QMimeData* mimeData = event->mimeData();
if (mimeData->hasUrls()) {
const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls());
if (!kdbxFiles.isEmpty()) {
event->acceptProposedAction();
}
}
}
void MainWindow::dropEvent(QDropEvent* event)
{
const QMimeData* mimeData = event->mimeData();
if (mimeData->hasUrls()) {
const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls());
if (!kdbxFiles.isEmpty()) {
event->acceptProposedAction();
}
for (const QString& kdbxFile: kdbxFiles) {
openDatabase(kdbxFile);
}
}
}

View File

@ -61,6 +61,7 @@ public slots:
void hideGlobalMessage();
void showYubiKeyPopup();
void hideYubiKeyPopup();
void bringToFront();
protected:
void closeEvent(QCloseEvent* event) override;
@ -107,6 +108,10 @@ private:
void updateTrayIcon();
bool isTrayIconEnabled() const;
static QStringList kdbxFilesFromUrls(const QList<QUrl>& urls);
void dragEnterEvent(QDragEnterEvent* event) override;
void dropEvent(QDropEvent* event) override;
const QScopedPointer<Ui::MainWindow> m_ui;
SignalMultiplexer m_actionMultiplexer;
QAction* m_clearHistoryAction;

View File

@ -57,22 +57,6 @@ int main(int argc, char** argv)
// don't set organizationName as that changes the return value of
// QStandardPaths::writableLocation(QDesktopServices::DataLocation)
if (app.isAlreadyRunning()) {
qWarning() << QCoreApplication::translate("Main", "Another instance of KeePassXC is already running.").toUtf8().constData();
return 0;
}
QApplication::setQuitOnLastWindowClosed(false);
if (!Crypto::init()) {
QString error = QCoreApplication::translate("Main",
"Fatal error while testing the cryptographic functions.");
error.append("\n");
error.append(Crypto::errorString());
MessageBox::critical(nullptr, QCoreApplication::translate("Main", "KeePassXC - Error"), error);
return 1;
}
QCommandLineParser parser;
parser.setApplicationDescription(QCoreApplication::translate("main", "KeePassXC - cross-platform password manager"));
parser.addPositionalArgument("filename", QCoreApplication::translate("main", "filenames of the password databases to open (*.kdbx)"), "[filename(s)]");
@ -93,7 +77,26 @@ int main(int argc, char** argv)
parser.addOption(pwstdinOption);
parser.process(app);
const QStringList args = parser.positionalArguments();
const QStringList fileNames = parser.positionalArguments();
if (app.isAlreadyRunning()) {
if (!fileNames.isEmpty()) {
app.sendFileNamesToRunningInstance(fileNames);
}
qWarning() << QCoreApplication::translate("Main", "Another instance of KeePassXC is already running.").toUtf8().constData();
return 0;
}
QApplication::setQuitOnLastWindowClosed(false);
if (!Crypto::init()) {
QString error = QCoreApplication::translate("Main",
"Fatal error while testing the cryptographic functions.");
error.append("\n");
error.append(Crypto::errorString());
MessageBox::critical(nullptr, QCoreApplication::translate("Main", "KeePassXC - Error"), error);
return 1;
}
if (parser.isSet(configOption)) {
Config::createConfigFromFile(parser.value(configOption));
@ -109,15 +112,10 @@ int main(int argc, char** argv)
MainWindow mainWindow;
app.setMainWindow(&mainWindow);
QObject::connect(&app, &Application::anotherInstanceStarted,
[&]() {
mainWindow.ensurePolished();
mainWindow.setWindowState(mainWindow.windowState() & ~Qt::WindowMinimized);
mainWindow.show();
mainWindow.raise();
mainWindow.activateWindow();
});
QObject::connect(&app, SIGNAL(anotherInstanceStarted()), &mainWindow, SLOT(bringToFront()));
QObject::connect(&app, SIGNAL(applicationActivated()), &mainWindow, SLOT(bringToFront()));
QObject::connect(&app, SIGNAL(openFile(QString)), &mainWindow, SLOT(openDatabase(QString)));
QObject::connect(&app, SIGNAL(quitSignalReceived()), &mainWindow, SLOT(appExit()), Qt::DirectConnection);
// start minimized if configured
bool minimizeOnStartup = config()->get("GUI/MinimizeOnStartup").toBool();
@ -130,20 +128,19 @@ int main(int argc, char** argv)
}
if (config()->get("OpenPreviousDatabasesOnStartup").toBool()) {
const QStringList filenames = config()->get("LastOpenedDatabases").toStringList();
for (int ii = filenames.size()-1; ii >= 0; ii--) {
QString filename = filenames.at(ii);
const QStringList fileNames = config()->get("LastOpenedDatabases").toStringList();
for (const QString& filename: fileNames) {
if (!filename.isEmpty() && QFile::exists(filename)) {
mainWindow.openDatabase(filename, QString(), QString());
mainWindow.openDatabase(filename);
}
}
}
for (int ii=0; ii < args.length(); ii++) {
QString filename = args[ii];
const bool pwstdin = parser.isSet(pwstdinOption);
for (const QString& filename: fileNames) {
if (!filename.isEmpty() && QFile::exists(filename)) {
QString password;
if (parser.isSet(pwstdinOption)) {
if (pwstdin) {
static QTextStream in(stdin, QIODevice::ReadOnly);
password = in.readLine();
}

View File

@ -952,6 +952,43 @@ void TestGui::testDatabaseLocking()
QCOMPARE(m_tabWidget->tabText(0).remove('&'), origDbName);
}
void TestGui::testDragAndDropKdbxFiles()
{
const int openedDatabasesCount = m_tabWidget->count();
const QString badDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NotDatabase.notkdbx"));
QMimeData badMimeData;
badMimeData.setUrls({QUrl::fromLocalFile(badDatabaseFilePath)});
QDragEnterEvent badDragEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier);
qApp->notify(m_mainWindow, &badDragEvent);
QCOMPARE(badDragEvent.isAccepted(), false);
QDropEvent badDropEvent(QPoint(1, 1), Qt::LinkAction, &badMimeData, Qt::LeftButton, Qt::NoModifier);
qApp->notify(m_mainWindow, &badDropEvent);
QCOMPARE(badDropEvent.isAccepted(), false);
QCOMPARE(m_tabWidget->count(), openedDatabasesCount);
const QString goodDatabaseFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"));
QMimeData goodMimeData;
goodMimeData.setUrls({QUrl::fromLocalFile(goodDatabaseFilePath)});
QDragEnterEvent goodDragEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier);
qApp->notify(m_mainWindow, &goodDragEvent);
QCOMPARE(goodDragEvent.isAccepted(), true);
QDropEvent goodDropEvent(QPoint(1, 1), Qt::LinkAction, &goodMimeData, Qt::LeftButton, Qt::NoModifier);
qApp->notify(m_mainWindow, &goodDropEvent);
QCOMPARE(goodDropEvent.isAccepted(), true);
QCOMPARE(m_tabWidget->count(), openedDatabasesCount + 1);
MessageBox::setNextAnswer(QMessageBox::No);
triggerAction("actionDatabaseClose");
Tools::wait(100);
QCOMPARE(m_tabWidget->count(), openedDatabasesCount);
}
void TestGui::cleanupTestCase()
{
delete m_mainWindow;

View File

@ -60,6 +60,7 @@ private slots:
void testDatabaseSettings();
void testKeePass1Import();
void testDatabaseLocking();
void testDragAndDropKdbxFiles();
private:
int addCannedEntries();