1 /*
2 * Copyright (C) 2012 Tobias Tangemann
3 * Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
4 * Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
5 *
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.
10 *
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.
15 *
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/>.
18 */
19
20 #include "Application.h"
21
22 #include "autotype/AutoType.h"
23 #include "core/Config.h"
24 #include "core/Global.h"
25 #include "core/Resources.h"
26 #include "gui/MainWindow.h"
27 #include "gui/osutils/OSUtils.h"
28 #include "gui/styles/dark/DarkStyle.h"
29 #include "gui/styles/light/LightStyle.h"
30
31 #include <QFileInfo>
32 #include <QFileOpenEvent>
33 #include <QLockFile>
34 #include <QSocketNotifier>
35 #include <QStandardPaths>
36 #include <QtNetwork/QLocalSocket>
37
38 #if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
39 #include "core/OSEventFilter.h"
40 #endif
41
42 #if defined(Q_OS_UNIX)
43 #include <signal.h>
44 #include <sys/socket.h>
45 #include <unistd.h>
46 #endif
47
48 namespace
49 {
50 constexpr int WaitTimeoutMSec = 150;
51 const char BlockSizeProperty[] = "blockSize";
52 } // namespace
53
Application(int & argc,char ** argv)54 Application::Application(int& argc, char** argv)
55 : QApplication(argc, argv)
56 #ifdef Q_OS_UNIX
57 , m_unixSignalNotifier(nullptr)
58 #endif
59 , m_alreadyRunning(false)
60 , m_lockFile(nullptr)
61 #if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
62 , m_osEventFilter(new OSEventFilter())
63 {
64 installNativeEventFilter(m_osEventFilter.data());
65 #else
66 {
67 #endif
68 #if defined(Q_OS_UNIX)
69 registerUnixSignals();
70 #endif
71
72 QString userName = qgetenv("USER");
73 if (userName.isEmpty()) {
74 userName = qgetenv("USERNAME");
75 }
76 QString identifier = "keepassxc";
77 if (!userName.isEmpty()) {
78 identifier += "-" + userName;
79 }
80 #ifdef QT_DEBUG
81 // In DEBUG mode don't interfere with Release instances
82 identifier += "-DEBUG";
83 #endif
84 QString lockName = identifier + ".lock";
85 m_socketName = identifier + ".socket";
86
87 // According to documentation we should use RuntimeLocation on *nixes, but even Qt doesn't respect
88 // this and creates sockets in TempLocation, so let's be consistent.
89 m_lockFile = new QLockFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + lockName);
90 m_lockFile->setStaleLockTime(0);
91 m_lockFile->tryLock();
92
93 m_lockServer.setSocketOptions(QLocalServer::UserAccessOption);
94 connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted()));
95 connect(&m_lockServer, SIGNAL(newConnection()), this, SLOT(processIncomingConnection()));
96
97 switch (m_lockFile->error()) {
98 case QLockFile::NoError:
99 // No existing lock was found, start listener
100 m_lockServer.listen(m_socketName);
101 break;
102 case QLockFile::LockFailedError: {
103 if (config()->get(Config::SingleInstance).toBool()) {
104 // Attempt to connect to the existing instance
105 QLocalSocket client;
106 for (int i = 0; i < 3; ++i) {
107 client.connectToServer(m_socketName);
108 if (client.waitForConnected(WaitTimeoutMSec)) {
109 // Connection succeeded, this will raise the existing window if minimized
110 client.abort();
111 m_alreadyRunning = true;
112 break;
113 }
114 }
115
116 if (!m_alreadyRunning) {
117 // If we get here then the original instance is likely dead
118 qWarning() << QObject::tr("Existing single-instance lock file is invalid. Launching new instance.")
119 .toUtf8()
120 .constData();
121
122 // forceably reset the lock file
123 m_lockFile->removeStaleLockFile();
124 m_lockFile->tryLock();
125 // start the listen server
126 m_lockServer.listen(m_socketName);
127 }
128 }
129 break;
130 }
131 default:
132 qWarning()
133 << QObject::tr("The lock file could not be created. Single-instance mode disabled.").toUtf8().constData();
134 }
135
136 connect(osUtils, &OSUtilsBase::interfaceThemeChanged, this, [this]() {
137 if (config()->get(Config::GUI_ApplicationTheme).toString() != "classic") {
138 applyTheme();
139 }
140 });
141 }
142
143 Application::~Application()
144 {
145 m_lockServer.close();
146 if (m_lockFile) {
147 m_lockFile->unlock();
148 delete m_lockFile;
149 }
150 }
151
152 void Application::applyTheme()
153 {
154 auto appTheme = config()->get(Config::GUI_ApplicationTheme).toString();
155 if (appTheme == "auto") {
156 appTheme = osUtils->isDarkMode() ? "dark" : "light";
157 #ifdef Q_OS_WIN
158 if (winUtils()->isHighContrastMode()) {
159 appTheme = "classic";
160 }
161 #endif
162 }
163 QPixmapCache::clear();
164 if (appTheme == "light") {
165 auto* s = new LightStyle;
166 setPalette(s->standardPalette());
167 setStyle(s);
168 } else if (appTheme == "dark") {
169 auto* s = new DarkStyle;
170 setPalette(s->standardPalette());
171 setStyle(s);
172 m_darkTheme = true;
173 } else {
174 // Classic mode, don't check for dark theme on Windows
175 // because Qt 5.x does not support it
176 #ifndef Q_OS_WIN
177 m_darkTheme = osUtils->isDarkMode();
178 #endif
179 QFile stylesheetFile(":/styles/base/classicstyle.qss");
180 if (stylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
181 setStyleSheet(stylesheetFile.readAll());
182 stylesheetFile.close();
183 }
184 }
185 }
186
187 bool Application::event(QEvent* event)
188 {
189 // Handle Apple QFileOpenEvent from finder (double click on .kdbx file)
190 if (event->type() == QEvent::FileOpen) {
191 emit openFile(static_cast<QFileOpenEvent*>(event)->file());
192 return true;
193 }
194 #ifdef Q_OS_MACOS
195 // restore main window when clicking on the docker icon
196 else if (event->type() == QEvent::ApplicationActivate) {
197 emit applicationActivated();
198 }
199 #endif
200
201 return QApplication::event(event);
202 }
203
204 #if defined(Q_OS_UNIX)
205 int Application::unixSignalSocket[2];
206
207 void Application::registerUnixSignals()
208 {
209 int result = ::socketpair(AF_UNIX, SOCK_STREAM, 0, unixSignalSocket);
210 Q_ASSERT(0 == result);
211 if (0 != result) {
212 // do not register handles when socket creation failed, otherwise
213 // application will be unresponsive to signals such as SIGINT or SIGTERM
214 return;
215 }
216
217 QVector<int> const handledSignals = {SIGQUIT, SIGINT, SIGTERM, SIGHUP};
218 for (auto s : handledSignals) {
219 struct sigaction sigAction;
220
221 sigAction.sa_handler = handleUnixSignal;
222 sigemptyset(&sigAction.sa_mask);
223 sigAction.sa_flags = 0 | SA_RESTART;
224 sigaction(s, &sigAction, nullptr);
225 }
226
227 m_unixSignalNotifier = new QSocketNotifier(unixSignalSocket[1], QSocketNotifier::Read, this);
228 connect(m_unixSignalNotifier, SIGNAL(activated(int)), this, SLOT(quitBySignal()));
229 }
230
231 void Application::handleUnixSignal(int sig)
232 {
233 switch (sig) {
234 case SIGQUIT:
235 case SIGINT:
236 case SIGTERM: {
237 char buf = 0;
238 Q_UNUSED(!::write(unixSignalSocket[0], &buf, sizeof(buf)));
239 return;
240 }
241 case SIGHUP:
242 return;
243 }
244 }
245
246 void Application::quitBySignal()
247 {
248 m_unixSignalNotifier->setEnabled(false);
249 char buf;
250 Q_UNUSED(!::read(unixSignalSocket[1], &buf, sizeof(buf)));
251 emit quitSignalReceived();
252 }
253 #endif
254
255 void Application::processIncomingConnection()
256 {
257 if (m_lockServer.hasPendingConnections()) {
258 QLocalSocket* socket = m_lockServer.nextPendingConnection();
259 socket->setProperty(BlockSizeProperty, 0);
260 connect(socket, SIGNAL(readyRead()), this, SLOT(socketReadyRead()));
261 }
262 }
263
264 void Application::socketReadyRead()
265 {
266 QLocalSocket* socket = qobject_cast<QLocalSocket*>(sender());
267 if (!socket) {
268 return;
269 }
270
271 QDataStream in(socket);
272 in.setVersion(QDataStream::Qt_5_0);
273
274 int blockSize = socket->property(BlockSizeProperty).toInt();
275 if (blockSize == 0) {
276 // Relies on the fact that QDataStream format streams a quint32 into sizeof(quint32) bytes
277 if (socket->bytesAvailable() < qint64(sizeof(quint32))) {
278 return;
279 }
280 in >> blockSize;
281 }
282
283 if (socket->bytesAvailable() < blockSize || in.atEnd()) {
284 socket->setProperty(BlockSizeProperty, blockSize);
285 return;
286 }
287
288 QStringList fileNames;
289 quint32 id;
290 in >> id;
291
292 // TODO: move constants to enum
293 switch (id) {
294 case 1:
295 in >> fileNames;
296 for (const QString& fileName : asConst(fileNames)) {
297 const QFileInfo fInfo(fileName);
298 if (fInfo.isFile() && fInfo.suffix().toLower() == "kdbx") {
299 emit openFile(fileName);
300 }
301 }
302
303 break;
304 case 2:
305 getMainWindow()->lockAllDatabases();
306 break;
307 }
308
309 socket->deleteLater();
310 }
311
312 bool Application::isAlreadyRunning() const
313 {
314 #ifdef QT_DEBUG
315 // In DEBUG mode we can run unlimited instances
316 return false;
317 #endif
318 return config()->get(Config::SingleInstance).toBool() && m_alreadyRunning;
319 }
320
321 /**
322 * Send to-open file names to the running UI instance
323 *
324 * @param fileNames - list of file names to open
325 * @return true if all operations succeeded (connection made, data sent, connection closed)
326 */
327 bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames)
328 {
329 QLocalSocket client;
330 client.connectToServer(m_socketName);
331 const bool connected = client.waitForConnected(WaitTimeoutMSec);
332 if (!connected) {
333 return false;
334 }
335
336 QByteArray data;
337 QDataStream out(&data, QIODevice::WriteOnly);
338 out.setVersion(QDataStream::Qt_5_0);
339 out << quint32(0); // reserve space for block size
340 out << quint32(1); // ID for file name send. TODO: move to enum
341 out << fileNames; // send file names to be opened
342 out.device()->seek(0);
343 out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size
344
345 const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec);
346 client.disconnectFromServer();
347 const bool disconnected =
348 client.state() == QLocalSocket::UnconnectedState || client.waitForDisconnected(WaitTimeoutMSec);
349 return writeOk && disconnected;
350 }
351
352 /**
353 * Locks all open databases in the running instance
354 *
355 * @return true if the "please lock" signal was sent successfully
356 */
357 bool Application::sendLockToInstance()
358 {
359 // Make a connection to avoid SIGSEGV
360 QLocalSocket client;
361 client.connectToServer(m_socketName);
362 const bool connected = client.waitForConnected(WaitTimeoutMSec);
363 if (!connected) {
364 return false;
365 }
366
367 // Send lock signal
368 QByteArray data;
369 QDataStream out(&data, QIODevice::WriteOnly);
370 out.setVersion(QDataStream::Qt_5_0);
371 out << quint32(0); // reserve space for block size
372 out << quint32(2); // ID for database lock. TODO: move to enum
373 out.device()->seek(0);
374 out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size
375
376 // Finish gracefully
377 const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec);
378 client.disconnectFromServer();
379 const bool disconnected =
380 client.state() == QLocalSocket::UnconnectedState || client.waitForConnected(WaitTimeoutMSec);
381 return writeOk && disconnected;
382 }
383
384 bool Application::isDarkTheme() const
385 {
386 return m_darkTheme;
387 }
388
389 void Application::restart()
390 {
391 // Disable single instance
392 m_lockServer.close();
393 if (m_lockFile) {
394 m_lockFile->unlock();
395 delete m_lockFile;
396 m_lockFile = nullptr;
397 }
398
399 exit(RESTART_EXITCODE);
400 }
401