Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qlockfile.cpp
Go to the documentation of this file.
1// Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
2// Copyright (C) 2016 The Qt Company Ltd.
3// Copyright (C) 2017 Intel Corporation.
4// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
5// Qt-Security score:critical reason:data-parser
6
7#include "qlockfile.h"
8#include "qlockfile_p.h"
9
10#include <QtCore/qthread.h>
11#include <QtCore/qcoreapplication.h>
12#include <QtCore/qdeadlinetimer.h>
13#include <QtCore/qdatetime.h>
14#include <QtCore/qfileinfo.h>
15
16#include <qplatformdefs.h>
17
18#ifdef Q_OS_WIN
19#include <io.h>
20#include <qt_windows.h>
21#endif
22
24
25using namespace Qt::StringLiterals;
26
27namespace {
28struct LockFileInfo
29{
30 qint64 pid;
31 QString appname;
32 QString hostname;
33 QByteArray hostid;
34 QByteArray bootid;
35};
36}
37
38static bool getLockInfo_helper(const QString &fileName, LockFileInfo *info);
39
41{
42#ifdef Q_OS_WIN
43 // we don't use QSysInfo because it tries to do name resolution
44 return qEnvironmentVariable("COMPUTERNAME");
45#else
46 return QSysInfo::machineHostName();
47#endif
48}
49
50/*!
51 \class QLockFile
52 \inmodule QtCore
53 \ingroup io
54 \brief The QLockFile class provides locking between processes using a file.
55 \since 5.1
56
57 A lock file can be used to prevent multiple processes from accessing concurrently
58 the same resource. For instance, a configuration file on disk, or a socket, a port,
59 a region of shared memory...
60
61 Serialization is only guaranteed if all processes that access the shared resource
62 use QLockFile, with the same file path.
63
64 QLockFile supports two use cases:
65 to protect a resource for a short-term operation (e.g. verifying if a configuration
66 file has changed before saving new settings), and for long-lived protection of a
67 resource (e.g. a document opened by a user in an editor) for an indefinite amount of time.
68
69 When protecting for a short-term operation, it is acceptable to call lock() and wait
70 until any running operation finishes.
71 When protecting a resource over a long time, however, the application should always
72 call setStaleLockTime(0ms) and then tryLock() with a short timeout, in order to
73 warn the user that the resource is locked.
74
75 If the process holding the lock crashes, the lock file stays on disk and can prevent
76 any other process from accessing the shared resource, ever. For this reason, QLockFile
77 tries to detect such a "stale" lock file, based on the process ID written into the file.
78 To cover the situation that the process ID got reused meanwhile, the current process name is
79 compared to the name of the process that corresponds to the process ID from the lock file.
80 If the process names differ, the lock file is considered stale.
81 Additionally, the last modification time of the lock file (30s by default, for the use case of a
82 short-lived operation) is taken into account.
83 If the lock file is found to be stale, it will be deleted.
84
85 For the use case of protecting a resource over a long time, you should therefore call
86 setStaleLockTime(0), and when tryLock() returns LockFailedError, inform the user
87 that the document is locked, possibly using getLockInfo() for more details.
88
89 \note On Windows, this class has problems detecting a stale lock if the
90 machine's hostname contains characters outside the US-ASCII character set.
91*/
92
93/*!
94 \enum QLockFile::LockError
95
96 This enum describes the result of the last call to lock() or tryLock().
97
98 \value NoError The lock was acquired successfully.
99 \value LockFailedError The lock could not be acquired because another process holds it.
100 \value PermissionError The lock file could not be created, for lack of permissions
101 in the parent directory.
102 \value UnknownError Another error happened, for instance a full partition
103 prevented writing out the lock file.
104*/
105
106/*!
107 Constructs a new lock file object.
108 The object is created in an unlocked state.
109 When calling lock() or tryLock(), a lock file named \a fileName will be created,
110 if it doesn't already exist.
111
112 \sa lock(), unlock()
113*/
114QLockFile::QLockFile(const QString &fileName)
115 : d_ptr(new QLockFilePrivate(fileName))
116{
117}
118
119/*!
120 Destroys the lock file object.
121 If the lock was acquired, this will release the lock, by deleting the lock file.
122*/
123QLockFile::~QLockFile()
124{
125 unlock();
126}
127
128/*!
129 * Returns the file name of the lock file
130 */
131QString QLockFile::fileName() const
132{
133 return d_ptr->fileName;
134}
135
136/*!
137 \fn void QLockFile::setStaleLockTime(int staleLockTime)
138
139 Sets \a staleLockTime to be the time in milliseconds after which
140 a lock file is considered stale.
141 The default value is 30000, i.e. 30 seconds.
142 If your application typically keeps the file locked for more than 30 seconds
143 (for instance while saving megabytes of data for 2 minutes), you should set
144 a bigger value using setStaleLockTime().
145
146 The value of \a staleLockTime is used by lock() and tryLock() in order
147 to determine when an existing lock file is considered stale, i.e. left over
148 by a crashed process. This is useful for the case where the PID got reused
149 meanwhile, so one way to detect a stale lock file is by the fact that
150 it has been around for a long time.
151
152 This is an overloaded function, equivalent to calling:
153 \code
154 setStaleLockTime(std::chrono::milliseconds{staleLockTime});
155 \endcode
156
157 \sa staleLockTime()
158*/
159
160/*!
161 \since 6.2
162
163 Sets the interval after which a lock file is considered stale to \a staleLockTime.
164 The default value is 30s.
165
166 If your application typically keeps the file locked for more than 30 seconds
167 (for instance while saving megabytes of data for 2 minutes), you should set
168 a bigger value using setStaleLockTime().
169
170 The value of staleLockTime() is used by lock() and tryLock() in order
171 to determine when an existing lock file is considered stale, i.e. left over
172 by a crashed process. This is useful for the case where the PID got reused
173 meanwhile, so one way to detect a stale lock file is by the fact that
174 it has been around for a long time.
175
176 \sa staleLockTime()
177*/
178void QLockFile::setStaleLockTime(std::chrono::milliseconds staleLockTime)
179{
180 Q_D(QLockFile);
181 d->staleLockTime = staleLockTime;
182}
183
184/*!
185 \fn int QLockFile::staleLockTime() const
186
187 Returns the time in milliseconds after which
188 a lock file is considered stale.
189
190 \sa setStaleLockTime()
191*/
192
193/*! \fn std::chrono::milliseconds QLockFile::staleLockTimeAsDuration() const
194 \overload
195 \since 6.2
196
197 Returns a std::chrono::milliseconds object which denotes the time after
198 which a lock file is considered stale.
199
200 \sa setStaleLockTime()
201*/
202std::chrono::milliseconds QLockFile::staleLockTimeAsDuration() const
203{
204 Q_D(const QLockFile);
205 return d->staleLockTime;
206}
207
208/*!
209 Returns \c true if the lock was acquired by this QLockFile instance,
210 otherwise returns \c false.
211
212 \sa lock(), unlock(), tryLock()
213*/
214bool QLockFile::isLocked() const
215{
216 Q_D(const QLockFile);
217 return d->isLocked;
218}
219
220/*!
221 Creates the lock file.
222
223 If another process (or another thread) has created the lock file already,
224 this function will block until that process (or thread) releases it.
225
226 Calling this function multiple times on the same lock from the same
227 thread without unlocking first is not allowed. This function will
228 \e dead-lock when the file is locked recursively.
229
230 Returns \c true if the lock was acquired, false if it could not be acquired
231 due to an unrecoverable error, such as no permissions in the parent directory.
232
233 \sa unlock(), tryLock()
234*/
235bool QLockFile::lock()
236{
237 return tryLock(std::chrono::milliseconds::max());
238}
239
240/*!
241 \fn bool QLockFile::tryLock(int timeout)
242
243 Attempts to create the lock file. This function returns \c true if the
244 lock was obtained; otherwise it returns \c false. If another process (or
245 another thread) has created the lock file already, this function will
246 wait for at most \a timeout milliseconds for the lock file to become
247 available.
248
249 Note: Passing a negative number as the \a timeout is equivalent to
250 calling lock(), i.e. this function will wait forever until the lock
251 file can be locked if \a timeout is negative.
252
253 If the lock was obtained, it must be released with unlock()
254 before another process (or thread) can successfully lock it.
255
256 Calling this function multiple times on the same lock from the same
257 thread without unlocking first is not allowed, this function will
258 \e always return false when attempting to lock the file recursively.
259
260 \sa lock(), unlock()
261*/
262
263/*!
264 \overload
265 \since 6.2
266
267 Attempts to create the lock file. This function returns \c true if the
268 lock was obtained; otherwise it returns \c false. If another process (or
269 another thread) has created the lock file already, this function will
270 wait for at most \a timeout for the lock file to become available.
271
272 If the lock was obtained, it must be released with unlock()
273 before another process (or thread) can successfully lock it.
274
275 Calling this function multiple times on the same lock from the same
276 thread without unlocking first is not allowed, this function will
277 \e always return false when attempting to lock the file recursively.
278
279 \sa lock(), unlock()
280*/
281bool QLockFile::tryLock(std::chrono::milliseconds timeout)
282{
283 using namespace std::chrono_literals;
284 using Msec = std::chrono::milliseconds;
285
286 Q_D(QLockFile);
287
288 QDeadlineTimer timer(timeout < 0ms ? Msec::max() : timeout);
289
290 Msec sleepTime = 100ms;
291 while (true) {
292 d->lockError = d->tryLock_sys();
293 switch (d->lockError) {
294 case NoError:
295 d->isLocked = true;
296 return true;
297 case PermissionError:
298 case UnknownError:
299 return false;
300 case LockFailedError:
301 if (!d->isLocked && d->isApparentlyStale()) {
302 if (Q_UNLIKELY(QFileInfo(d->fileName).lastModified(QTimeZone::UTC) > QDateTime::currentDateTimeUtc()))
303 qInfo("QLockFile: Lock file '%ls' has a modification time in the future", qUtf16Printable(d->fileName));
304 // Stale lock from another thread/process
305 // Ensure two processes don't remove it at the same time
306 QLockFile rmlock(d->fileName + ".rmlock"_L1);
307 if (rmlock.tryLock()) {
308 if (d->isApparentlyStale() && d->removeStaleLock())
309 continue;
310 }
311 }
312 break;
313 }
314
315 auto remainingTime = std::chrono::duration_cast<Msec>(timer.remainingTimeAsDuration());
316 if (remainingTime == 0ms)
317 return false;
318
319 if (sleepTime > remainingTime)
320 sleepTime = remainingTime;
321
322 QThread::sleep(sleepTime);
323 if (sleepTime < 5s)
324 sleepTime *= 2;
325 }
326 // not reached
327 return false;
328}
329
330/*!
331 \fn void QLockFile::unlock()
332 Releases the lock, by deleting the lock file.
333
334 Calling unlock() without locking the file first, does nothing.
335
336 \sa lock(), tryLock()
337*/
338
339/*!
340 Retrieves information about the current owner of the lock file.
341
342 If tryLock() returns \c false, and error() returns LockFailedError,
343 this function can be called to find out more information about the existing
344 lock file:
345 \list
346 \li the PID of the application (returned in \a pid)
347 \li the \a hostname it's running on (useful in case of networked filesystems),
348 \li the name of the application which created it (returned in \a appname),
349 \endlist
350
351 Note that tryLock() automatically deleted the file if there is no
352 running application with this PID, so LockFailedError can only happen if there is
353 an application with this PID (it could be unrelated though).
354
355 This can be used to inform users about the existing lock file and give them
356 the choice to delete it. After removing the file using removeStaleLockFile(),
357 the application can call tryLock() again.
358
359 This function returns \c true if the information could be successfully retrieved, false
360 if the lock file doesn't exist or doesn't contain the expected data.
361 This can happen if the lock file was deleted between the time where tryLock() failed
362 and the call to this function. Simply call tryLock() again if this happens.
363*/
364bool QLockFile::getLockInfo(qint64 *pid, QString *hostname, QString *appname) const
365{
366 Q_D(const QLockFile);
367 LockFileInfo info;
368 if (!getLockInfo_helper(d->fileName, &info))
369 return false;
370 if (pid)
371 *pid = info.pid;
372 if (hostname)
373 *hostname = info.hostname;
374 if (appname)
375 *appname = info.appname;
376 return true;
377}
378
380 : fileName(fn),
381#ifdef Q_OS_WIN
383#else
384 fileHandle(-1)
385#endif
386{
387}
388
390 = default;
391
393{
394 // Use operator% from the fast builder to avoid multiple memory allocations.
395 return QByteArray::number(QCoreApplication::applicationPid()) % '\n'
396 % processNameByPid(QCoreApplication::applicationPid()).toUtf8() % '\n'
397 % machineName().toUtf8() % '\n'
398 % QSysInfo::machineUniqueId() % '\n'
399 % QSysInfo::bootUniqueId() % '\n';
400}
401
402static bool getLockInfo_helper(const QString &fileName, LockFileInfo *info)
403{
404 QFile reader(fileName);
405 if (!reader.open(QIODevice::ReadOnly | QIODevice::Text))
406 return false;
407
408 QByteArray pidLine = reader.readLine();
409 pidLine.chop(1);
410 if (pidLine.isEmpty())
411 return false;
412 QByteArray appNameLine = reader.readLine();
413 appNameLine.chop(1);
414 QByteArray hostNameLine = reader.readLine();
415 hostNameLine.chop(1);
416
417 // prior to Qt 5.10, only the lines above were recorded
418 QByteArray hostId = reader.readLine();
419 hostId.chop(1);
420 QByteArray bootId = reader.readLine();
421 bootId.chop(1);
422
423 bool ok;
424 info->appname = QString::fromUtf8(appNameLine);
425 info->hostname = QString::fromUtf8(hostNameLine);
426 info->hostid = hostId;
427 info->bootid = bootId;
428 info->pid = pidLine.toLongLong(&ok);
429 return ok && info->pid > 0;
430}
431
433{
434 LockFileInfo info;
435 if (getLockInfo_helper(fileName, &info)) {
436 bool sameHost = info.hostname.isEmpty() || info.hostname == machineName();
437 if (!info.hostid.isEmpty()) {
438 // Override with the host ID, if we know it.
439 QByteArray ourHostId = QSysInfo::machineUniqueId();
440 if (!ourHostId.isEmpty())
441 sameHost = (ourHostId == info.hostid);
442 }
443
444 if (sameHost) {
445 if (!info.bootid.isEmpty()) {
446 // If we've rebooted, then the lock is definitely stale.
447 if (info.bootid != QSysInfo::bootUniqueId())
448 return true;
449 }
450 if (!isProcessRunning(info.pid, info.appname))
451 return true;
452 }
453 }
454
455 const QDateTime lastMod = QFileInfo(fileName).lastModified(QTimeZone::UTC);
456 using namespace std::chrono;
457 const milliseconds age{lastMod.msecsTo(QDateTime::currentDateTimeUtc())};
458 return staleLockTime > 0ms && abs(age) > staleLockTime;
459}
460
461
462
463/*!
464 Attempts to forcefully remove an existing lock file.
465
466 Calling this is not recommended when protecting a short-lived operation: QLockFile
467 already takes care of removing lock files after they are older than staleLockTime().
468
469 This method should only be called when protecting a resource for a long time, i.e.
470 with staleLockTime(0), and after tryLock() returned LockFailedError, and the user
471 agreed on removing the lock file.
472
473 Returns \c true on success, false if the lock file couldn't be removed. This happens
474 on Windows, when the application owning the lock is still running.
475*/
476bool QLockFile::removeStaleLockFile()
477{
478 Q_D(QLockFile);
479 if (d->isLocked) {
480 qWarning("removeStaleLockFile can only be called when not holding the lock");
481 return false;
482 }
483 return d->removeStaleLock();
484}
485
486/*!
487 Returns the lock file error status.
488
489 If tryLock() returns \c false, this function can be called to find out
490 the reason why the locking failed.
491*/
492QLockFile::LockError QLockFile::error() const
493{
494 Q_D(const QLockFile);
495 return d->lockError;
496}
497
498QT_END_NAMESPACE
QLockFilePrivate(const QString &fn)
bool isApparentlyStale() const
QByteArray lockFileContents() const
static QString machineName()
Definition qlockfile.cpp:40
static bool getLockInfo_helper(const QString &fileName, LockFileInfo *info)