2
* Copyright (C) 2012 Tobias Tangemann
3
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
4
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
6
* This program is free software: you can redistribute it and/or modify
7
* it under the terms of the GNU General Public License as published by
8
* the Free Software Foundation, either version 2 or (at your option)
9
* version 3 of the License.
11
* This program is distributed in the hope that it will be useful,
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
* GNU General Public License for more details.
16
* You should have received a copy of the GNU General Public License
17
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20
#include "Application.h"
22
#include "core/Bootstrap.h"
23
#include "gui/MainWindow.h"
24
#include "gui/MessageBox.h"
25
#include "gui/osutils/OSUtils.h"
26
#include "gui/styles/dark/DarkStyle.h"
27
#include "gui/styles/light/LightStyle.h"
30
#include <QFileOpenEvent>
31
#include <QLocalSocket>
33
#include <QPixmapCache>
34
#include <QSocketNotifier>
35
#include <QStandardPaths>
39
#include <sys/socket.h>
45
constexpr int WaitTimeoutMSec = 150;
46
const char BlockSizeProperty[] = "blockSize";
49
Application::Application(int& argc, char** argv)
50
: QApplication(argc, argv)
52
, m_unixSignalNotifier(nullptr)
54
, m_alreadyRunning(false)
56
#if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
62
registerUnixSignals();
65
QString userName = qgetenv("USER");
66
if (userName.isEmpty()) {
67
userName = qgetenv("USERNAME");
69
QString identifier = "keepassxc";
70
if (!userName.isEmpty()) {
71
identifier += "-" + userName;
74
// In DEBUG mode don't interfere with Release instances
75
identifier += "-DEBUG";
77
QString lockName = identifier + ".lock";
78
m_socketName = identifier + ".socket";
80
// According to documentation we should use RuntimeLocation on *nixes, but even Qt doesn't respect
81
// this and creates sockets in TempLocation, so let's be consistent.
82
m_lockFile = new QLockFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + lockName);
83
m_lockFile->setStaleLockTime(0);
84
m_lockFile->tryLock();
86
m_lockServer.setSocketOptions(QLocalServer::UserAccessOption);
87
connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted()));
88
connect(&m_lockServer, SIGNAL(newConnection()), this, SLOT(processIncomingConnection()));
90
switch (m_lockFile->error()) {
91
case QLockFile::NoError:
92
// No existing lock was found, start listener
93
m_lockServer.listen(m_socketName);
95
case QLockFile::LockFailedError: {
96
if (config()->get(Config::SingleInstance).toBool()) {
97
// Attempt to connect to the existing instance
99
for (int i = 0; i < 3; ++i) {
100
client.connectToServer(m_socketName);
101
if (client.waitForConnected(WaitTimeoutMSec)) {
102
// Connection succeeded, this will raise the existing window if minimized
104
m_alreadyRunning = true;
109
if (!m_alreadyRunning) {
110
// If we get here then the original instance is likely dead
111
qWarning() << QObject::tr("Existing single-instance lock file is invalid. Launching new instance.")
115
// forceably reset the lock file
116
m_lockFile->removeStaleLockFile();
117
m_lockFile->tryLock();
118
// start the listen server
119
m_lockServer.listen(m_socketName);
126
<< QObject::tr("The lock file could not be created. Single-instance mode disabled.").toUtf8().constData();
129
connect(osUtils, &OSUtilsBase::interfaceThemeChanged, this, [this]() {
130
if (config()->get(Config::GUI_ApplicationTheme).toString() != "classic") {
136
Application::~Application()
138
m_lockServer.close();
140
m_lockFile->unlock();
146
* Perform early application bootstrapping such as setting up search paths,
147
* configuration OS security properties, and loading translators.
148
* A QApplication object has to be instantiated before calling this function.
150
void Application::bootstrap(const QString& uiLanguage)
152
Bootstrap::bootstrap(uiLanguage);
155
// Qt on Windows uses "MS Shell Dlg 2" as the default font for many widgets, which resolves
156
// to Tahoma 8pt, whereas the correct font would be "Segoe UI" 9pt.
157
// Apparently, some widgets are already using the correct font. Thanks, MuseScore for this neat fix!
158
QApplication::setFont(QApplication::font("QMessageBox"));
161
osUtils->registerNativeEventFilter();
162
MessageBox::initializeButtonDefs();
165
// Don't show menu icons on OSX
166
QApplication::setAttribute(Qt::AA_DontShowIconsInMenus);
170
void Application::applyTheme()
172
auto appTheme = config()->get(Config::GUI_ApplicationTheme).toString();
173
if (appTheme == "auto") {
174
appTheme = osUtils->isDarkMode() ? "dark" : "light";
176
if (winUtils()->isHighContrastMode()) {
177
appTheme = "classic";
181
QPixmapCache::clear();
182
if (appTheme == "light") {
183
auto* s = new LightStyle;
184
setPalette(s->standardPalette());
187
} else if (appTheme == "dark") {
188
auto* s = new DarkStyle;
189
setPalette(s->standardPalette());
193
// Classic mode, don't check for dark theme on Windows
194
// because Qt 5.x does not support it
198
m_darkTheme = osUtils->isDarkMode();
200
QFile stylesheetFile(":/styles/base/classicstyle.qss");
201
if (stylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
202
setStyleSheet(stylesheetFile.readAll());
203
stylesheetFile.close();
208
bool Application::event(QEvent* event)
210
// Handle Apple QFileOpenEvent from finder (double click on .kdbx file)
211
if (event->type() == QEvent::FileOpen) {
212
emit openFile(static_cast<QFileOpenEvent*>(event)->file());
216
// restore main window when clicking on the docker icon
217
else if (event->type() == QEvent::ApplicationActivate) {
218
emit applicationActivated();
222
return QApplication::event(event);
225
#if defined(Q_OS_UNIX)
226
int Application::unixSignalSocket[2];
228
void Application::registerUnixSignals()
230
int result = ::socketpair(AF_UNIX, SOCK_STREAM, 0, unixSignalSocket);
231
Q_ASSERT(0 == result);
233
// do not register handles when socket creation failed, otherwise
234
// application will be unresponsive to signals such as SIGINT or SIGTERM
238
QVector<int> const handledSignals = {SIGQUIT, SIGINT, SIGTERM, SIGHUP};
239
for (auto s : handledSignals) {
240
struct sigaction sigAction;
242
sigAction.sa_handler = handleUnixSignal;
243
sigemptyset(&sigAction.sa_mask);
244
sigAction.sa_flags = 0 | SA_RESTART;
245
sigaction(s, &sigAction, nullptr);
248
m_unixSignalNotifier = new QSocketNotifier(unixSignalSocket[1], QSocketNotifier::Read, this);
249
connect(m_unixSignalNotifier, SIGNAL(activated(int)), this, SLOT(quitBySignal()));
252
void Application::handleUnixSignal(int sig)
259
Q_UNUSED(!::write(unixSignalSocket[0], &buf, sizeof(buf)));
267
void Application::quitBySignal()
269
m_unixSignalNotifier->setEnabled(false);
271
Q_UNUSED(!::read(unixSignalSocket[1], &buf, sizeof(buf)));
272
emit quitSignalReceived();
276
void Application::processIncomingConnection()
278
if (m_lockServer.hasPendingConnections()) {
279
QLocalSocket* socket = m_lockServer.nextPendingConnection();
280
socket->setProperty(BlockSizeProperty, 0);
281
connect(socket, SIGNAL(readyRead()), this, SLOT(socketReadyRead()));
285
void Application::socketReadyRead()
287
auto socket = qobject_cast<QLocalSocket*>(sender());
292
QDataStream in(socket);
293
in.setVersion(QDataStream::Qt_5_0);
295
int blockSize = socket->property(BlockSizeProperty).toInt();
296
if (blockSize == 0) {
297
// Relies on the fact that QDataStream format streams a quint32 into sizeof(quint32) bytes
298
if (socket->bytesAvailable() < qint64(sizeof(quint32))) {
304
if (socket->bytesAvailable() < blockSize || in.atEnd()) {
305
socket->setProperty(BlockSizeProperty, blockSize);
309
QStringList fileNames;
313
// TODO: move constants to enum
317
for (const QString& fileName : asConst(fileNames)) {
318
const QFileInfo fInfo(fileName);
319
if (fInfo.isFile() && fInfo.suffix().toLower() == "kdbx") {
320
emit openFile(fileName);
326
getMainWindow()->lockAllDatabases();
330
socket->deleteLater();
333
bool Application::isAlreadyRunning() const
336
// In DEBUG mode we can run unlimited instances
339
return config()->get(Config::SingleInstance).toBool() && m_alreadyRunning;
343
* Send to-open file names to the running UI instance
345
* @param fileNames - list of file names to open
346
* @return true if all operations succeeded (connection made, data sent, connection closed)
348
bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames)
351
client.connectToServer(m_socketName);
352
const bool connected = client.waitForConnected(WaitTimeoutMSec);
358
QDataStream out(&data, QIODevice::WriteOnly);
359
out.setVersion(QDataStream::Qt_5_0);
360
out << quint32(0); // reserve space for block size
361
out << quint32(1); // ID for file name send. TODO: move to enum
362
out << fileNames; // send file names to be opened
363
out.device()->seek(0);
364
out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size
366
const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec);
367
client.disconnectFromServer();
368
const bool disconnected =
369
client.state() == QLocalSocket::UnconnectedState || client.waitForDisconnected(WaitTimeoutMSec);
370
return writeOk && disconnected;
374
* Locks all open databases in the running instance
376
* @return true if the "please lock" signal was sent successfully
378
bool Application::sendLockToInstance()
380
// Make a connection to avoid SIGSEGV
382
client.connectToServer(m_socketName);
383
const bool connected = client.waitForConnected(WaitTimeoutMSec);
390
QDataStream out(&data, QIODevice::WriteOnly);
391
out.setVersion(QDataStream::Qt_5_0);
392
out << quint32(0); // reserve space for block size
393
out << quint32(2); // ID for database lock. TODO: move to enum
394
out.device()->seek(0);
395
out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size
398
const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec);
399
client.disconnectFromServer();
400
const bool disconnected =
401
client.state() == QLocalSocket::UnconnectedState || client.waitForConnected(WaitTimeoutMSec);
402
return writeOk && disconnected;
405
bool Application::isDarkTheme() const
410
void Application::restart()
412
// Disable single instance
413
m_lockServer.close();
415
m_lockFile->unlock();
417
m_lockFile = nullptr;
420
exit(RESTART_EXITCODE);