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/qcoreapplication.h>
11#include <QtCore/qdeadlinetimer.h>
12#include <QtCore/qdatetime.h>
13#include <QtCore/qfileinfo.h>
14#include <QtCore/private/qfilesystemengine_p.h>
15#include <QtCore/qrandom.h>
16#include <QtCore/qthread.h>
17#include <QtCore/private/quniquehandle_types_p.h>
18
19#include <qplatformdefs.h>
20
21#ifdef Q_OS_WIN
22#include <io.h>
23#include <qt_windows.h>
24#endif
25
27
28using namespace Qt::StringLiterals;
29
31{
32#ifdef Q_OS_WIN
33 // we don't use QSysInfo because it tries to do name resolution
34 return qEnvironmentVariable("COMPUTERNAME");
35#else
36 return QSysInfo::machineHostName();
37#endif
38}
39
40/*!
41 \class QLockFile
42 \inmodule QtCore
43 \ingroup io
44 \brief The QLockFile class provides locking between processes using a file.
45 \since 5.1
46
47 A lock file can be used to prevent multiple processes from accessing concurrently
48 the same resource. For instance, a configuration file on disk, or a socket, a port,
49 a region of shared memory...
50
51 Serialization is only guaranteed if all processes that access the shared resource
52 use QLockFile, with the same file path.
53
54 QLockFile supports two use cases:
55 to protect a resource for a short-term operation (e.g. verifying if a configuration
56 file has changed before saving new settings), and for long-lived protection of a
57 resource (e.g. a document opened by a user in an editor) for an indefinite amount of time.
58
59 When protecting for a short-term operation, it is acceptable to call lock() and wait
60 until any running operation finishes.
61 When protecting a resource over a long time, however, the application should always
62 call setStaleLockTime(0ms) and then tryLock() with a short timeout, in order to
63 warn the user that the resource is locked.
64
65 If the process holding the lock crashes, the lock file stays on disk and can prevent
66 any other process from accessing the shared resource, ever. For this reason, QLockFile
67 tries to detect such a "stale" lock file, based on the process ID written into the file.
68 To cover the situation that the process ID got reused meanwhile, the current process name is
69 compared to the name of the process that corresponds to the process ID from the lock file.
70 If the process names differ, the lock file is considered stale.
71 Additionally, the last modification time of the lock file (30s by default, for the use case of a
72 short-lived operation) is taken into account.
73 If the lock file is found to be stale, it will be deleted.
74
75 For the use case of protecting a resource over a long time, you should therefore call
76 setStaleLockTime(0), and when tryLock() returns LockFailedError, inform the user
77 that the document is locked, possibly using getLockInfo() for more details.
78
79 \section1 Limitations
80
81 QLockFile's implementation is usually safe, for the majority of
82 environments and filesystems. There are a few situations in which it can
83 incorrectly conclude a lock file is stale when it isn't, fail to detect a
84 stale file, or race when locking. This section documents those issues.
85
86 There are two main mitigation strategies: choosing a suitable, non-zero
87 staleLockTime() and choosing a regular, local filesystem for storing lock
88 files (such as \c{/var/lock} if running as root on Unix systems or the
89 \l{QStandardPaths::RuntimeLocation}{runtime location}). That may not be
90 possible for certain use-cases of QLockFile, such as when using QLockFile
91 to indicate a file path provided by the user is being edited.
92
93 \section2 Non-persistent machine IDs
94
95 QLockFile uses the machine's unique ID to differentiate a lock by the
96 current machine and one by a process running on a different host. If the
97 machine ID changes over time (such as on systems with ephemeral storage,
98 which must generate a new ID at every boot), QLockFile will be unable to
99 detect that a lock file is stale after a reboot.
100
101 Conversely, if two different machines have colliding machine IDs (for
102 example, a cloned system image or restoration from backups) and provide a
103 network path to QLockFile, the class may conclude the lock is stale when it
104 actually is not.
105
106 \section2 Absence of native locking
107
108 QLockFile uses operating-system specific calls to indicate to other
109 processes and threads that the lock is alive (not stale), even past the
110 staleLockTime() setting. This functionality may be absent for files on some
111 networked or virtual filesystems (such as FUSE on Linux and \macos), when
112 using different filesystems to access the same file if the native lock
113 isn't carried through, or in certain environments that block the necessary
114 system calls.
115
116 If the native locking support is absent, QLockFile will rely solely on the
117 existence, modification time, and contents of the lock file itself. In this
118 case, if the process currently locking the resource keeps it past
119 staleLockTime(), QLockFile will steal the lock.
120
121 \section2 Networked filesystems
122
123 It is unspecified whether the native file-locking is supported in networked
124 environments: with some implementations, it is supported for all clients
125 accessing the filesystem; for others the local system may support locking
126 for its own processes but will not share the locking over the network; and
127 for yet others there is no native locking even inside one host. Moreover,
128 it is possible that some clients participate in networked locking and some
129 others do not, for the same file. If locking is not supported across the
130 network, QLockFile will observe the limitations described above for the
131 absence of native locking.
132
133 Additionally, if the lock file contains the identification of a different
134 host, QLockFile will be unable to confirm the process holding a lock is
135 still running, and will need to wait staleLockTime() to recover from an
136 unclean exit.
137
138 \section2 Accessing different actual files through the same path
139
140 It is possible for two processes to have different views of the filesystem,
141 causing an identical file path to be different files in the filesystem. In
142 this case, the two QLockFile objects will likely succeed at creating the
143 lock, but will not be mutually exclusive. This may cause conflicts if the
144 resource the lock file is protecting is still shared between them.
145
146 This situation is most often encountered with containers (see below), but
147 is not exclusive to them.
148
149 \section2 Files shared with containers
150
151 With some container implementations, it is possible to hide the existence
152 of some processes (for example, Linux's "PID namespace" feature). If a
153 process is running inside of such a container but shares the machine ID of
154 the host system or another container, two processes in different containers
155 (or the host) will make incorrect determinations on whether the locking
156 process is still running. Moreover, some container controllers may replace
157 the boot ID inside of the container, causing the launched processes to
158 conclude the lock file is always stale, regardless of how fresh its
159 timestamp is.
160
161 With some other implementations, containers may have ephemeral storage or
162 intentionally create a new machine ID to avoid collision. In this case, the
163 application will experience the problems described above for non-persistent
164 machine IDs.
165
166 However, the native file-locking usually works (subject to filesystem and
167 environment limitations as discussed above), so even if QLockFile did
168 conclude the file is apparently stale, it won't steal a lock file that is
169 natively locked.
170
171 \section2 Accesses not using the same protocol
172
173 QLockFile cannot interoperate with modifications to the lock file performed
174 outside of the protocol implemented by this class. This includes removal of
175 the lock file by other tools, such as tmpwatch and similar, but also some
176 virus-scanning or similar tools.
177
178 \section2 Clock skew
179
180 QLockFile relies on the time stamp of the lock file being accurate. If the
181 clock jumps forward, QLockFile may conclude a lock file has become stale
182 when it hasn't, and vice-versa for jumping backwards.
183
184 For this reason, it is recommended all systems keep network-synchronized
185 time and perform this synchronization early in their boot process. This is
186 particularly important for networked filesystems.
187*/
188
189/*!
190 \enum QLockFile::LockError
191
192 This enum describes the result of the last call to lock() or tryLock().
193
194 \value NoError The lock was acquired successfully.
195 \value LockFailedError The lock could not be acquired because another process holds it.
196 \value PermissionError The lock file could not be created, for lack of permissions
197 in the parent directory.
198 \value UnknownError Another error happened, for instance a full partition
199 prevented writing out the lock file.
200*/
201
202/*!
203 Constructs a new lock file object.
204 The object is created in an unlocked state.
205 When calling lock() or tryLock(), a lock file named \a fileName will be created,
206 if it doesn't already exist.
207
208 \sa lock(), unlock()
209*/
210QLockFile::QLockFile(const QString &fileName)
211 : d_ptr(new QLockFilePrivate(fileName))
212{
213}
214
215/*!
216 Destroys the lock file object.
217 If the lock was acquired, this will release the lock, by deleting the lock file.
218*/
219QLockFile::~QLockFile()
220{
221 unlock();
222}
223
224/*!
225 * Returns the file name of the lock file
226 */
227QString QLockFile::fileName() const
228{
229 return d_ptr->fileName;
230}
231
232/*!
233 \fn void QLockFile::setStaleLockTime(int staleLockTime)
234
235 Sets \a staleLockTime to be the time in milliseconds after which
236 a lock file is considered stale.
237 The default value is 30000, i.e. 30 seconds.
238 If your application typically keeps the file locked for more than 30 seconds
239 (for instance while saving megabytes of data for 2 minutes), you should set
240 a bigger value using setStaleLockTime().
241
242 The value of \a staleLockTime is used by lock() and tryLock() in order
243 to determine when an existing lock file is considered stale, i.e. left over
244 by a crashed process. This is useful for the case where the PID got reused
245 meanwhile, so one way to detect a stale lock file is by the fact that
246 it has been around for a long time.
247
248 This is an overloaded function, equivalent to calling:
249 \code
250 setStaleLockTime(std::chrono::milliseconds{staleLockTime});
251 \endcode
252
253 \sa staleLockTime()
254*/
255
256/*!
257 \since 6.2
258
259 Sets the interval after which a lock file is considered stale to \a staleLockTime.
260 The default value is 30s.
261
262 If your application typically keeps the file locked for more than 30 seconds
263 (for instance while saving megabytes of data for 2 minutes), you should set
264 a bigger value using setStaleLockTime().
265
266 The value of staleLockTime() is used by lock() and tryLock() in order
267 to determine when an existing lock file is considered stale, i.e. left over
268 by a crashed process. This is useful for the case where the PID got reused
269 meanwhile, so one way to detect a stale lock file is by the fact that
270 it has been around for a long time.
271
272 Setting this value to 0 or negative will disable the verification of
273 timestamps on lock files. QLockFile will still detect a stale lock if its
274 contents show that the locking process is no longer running.
275
276 \sa staleLockTime()
277*/
278void QLockFile::setStaleLockTime(std::chrono::milliseconds staleLockTime)
279{
280 Q_D(QLockFile);
281 d->staleLockTime = staleLockTime;
282}
283
284/*!
285 \fn int QLockFile::staleLockTime() const
286
287 Returns the time in milliseconds after which
288 a lock file is considered stale.
289
290 \sa setStaleLockTime()
291*/
292
293/*! \fn std::chrono::milliseconds QLockFile::staleLockTimeAsDuration() const
294 \overload
295 \since 6.2
296
297 Returns a std::chrono::milliseconds object which denotes the time after
298 which a lock file is considered stale.
299
300 \sa setStaleLockTime()
301*/
302std::chrono::milliseconds QLockFile::staleLockTimeAsDuration() const
303{
304 Q_D(const QLockFile);
305 return d->staleLockTime;
306}
307
308/*!
309 Returns \c true if the lock was acquired by this QLockFile instance,
310 otherwise returns \c false.
311
312 \sa lock(), unlock(), tryLock()
313*/
314bool QLockFile::isLocked() const
315{
316 Q_D(const QLockFile);
317 return d->isLocked;
318}
319
320/*!
321 Creates the lock file.
322
323 If another process (or another thread) has created the lock file already,
324 this function will block until that process (or thread) releases it.
325
326 Calling this function multiple times on the same lock from the same
327 thread without unlocking first is not allowed. This function will
328 \e dead-lock when the file is locked recursively.
329
330 Returns \c true if the lock was acquired, false if it could not be acquired
331 due to an unrecoverable error, such as no permissions in the parent directory.
332
333 \sa unlock(), tryLock()
334*/
335bool QLockFile::lock()
336{
337 return tryLock(std::chrono::milliseconds::max());
338}
339
340/*!
341 \fn bool QLockFile::tryLock(int timeout)
342
343 Attempts to create the lock file. This function returns \c true if the
344 lock was obtained; otherwise it returns \c false. If another process (or
345 another thread) has created the lock file already, this function will
346 wait for at most \a timeout milliseconds for the lock file to become
347 available.
348
349 Note: Passing a negative number as the \a timeout is equivalent to
350 calling lock(), i.e. this function will wait forever until the lock
351 file can be locked if \a timeout is negative.
352
353 If the lock was obtained, it must be released with unlock()
354 before another process (or thread) can successfully lock it.
355
356 Calling this function multiple times on the same lock from the same
357 thread without unlocking first is not allowed, this function will
358 \e always return false when attempting to lock the file recursively.
359
360 \sa lock(), unlock()
361*/
362
363/*!
364 \overload
365 \since 6.2
366
367 Attempts to create the lock file. This function returns \c true if the
368 lock was obtained; otherwise it returns \c false. If another process (or
369 another thread) has created the lock file already, this function will
370 wait for at most \a timeout for the lock file to become available.
371
372 If the lock was obtained, it must be released with unlock()
373 before another process (or thread) can successfully lock it.
374
375 Calling this function multiple times on the same lock from the same
376 thread without unlocking first is not allowed, this function will
377 \e always return false when attempting to lock the file recursively.
378
379 \sa lock(), unlock()
380*/
381bool QLockFile::tryLock(std::chrono::milliseconds timeout)
382{
383 using namespace std::chrono_literals;
384 constexpr int Perturbation = 4096 * 1024; // ns
385 using Msec = std::chrono::milliseconds;
386
387 Q_D(QLockFile);
388 QLockFilePrivate::LockFileInfo current(QLockFilePrivate::LockFileInfo::Current{});
389
390 // Don't convert \a timeout to a different duration type!
391 QDeadlineTimer timer(timeout < 0ms ? Msec::max() : timeout);
392
393 // Add a small perturbation to the sleep time, just in case two or more
394 // processes/threads are getting woken up together.
395 auto sleepTime = 100ms +
396 QRandomGenerator::global()->bounded(-Perturbation, Perturbation) * 1ns;
397
398 while (true) {
399 d->lockError = d->tryLock_sys(current);
400 switch (d->lockError) {
401 case NoError:
402 d->isLocked = true;
403 return true;
404 case PermissionError:
405 case UnknownError:
406 return false;
407 case LockFailedError:
408 if (!d->isLocked && d->isApparentlyStale(current)) {
409 if (Q_UNLIKELY(QFileInfo(d->fileName).lastModified(QTimeZone::UTC) > QDateTime::currentDateTimeUtc()))
410 qInfo("QLockFile: Lock file '%ls' has a modification time in the future", qUtf16Printable(d->fileName));
411 // Stale lock from another thread/process
412 // Ensure two processes don't remove it at the same time
413 QLockFile rmlock(d->fileName + ".rmlock"_L1);
414 if (rmlock.tryLock()) {
415 if (d->isApparentlyStale(current) && d->removeStaleLock())
416 continue;
417 }
418 }
419 break;
420 }
421
422 auto remainingTime = timer.remainingTimeAsDuration();
423 if (remainingTime == 0ms)
424 return false;
425
426 if (sleepTime > remainingTime)
427 sleepTime = remainingTime;
428
429 QThread::sleep(sleepTime);
430 if (sleepTime < 5s)
431 sleepTime *= 2;
432 }
433 // not reached
434 return false;
435}
436
437/*!
438 \fn void QLockFile::unlock()
439 Releases the lock, by deleting the lock file.
440
441 Calling unlock() without locking the file first, does nothing.
442
443 \sa lock(), tryLock()
444*/
445
446/*!
447 Retrieves information about the current owner of the lock file.
448
449 If tryLock() returns \c false, and error() returns LockFailedError,
450 this function can be called to find out more information about the existing
451 lock file:
452 \list
453 \li the PID of the application (returned in \a pid)
454 \li the \a hostname it's running on (useful in case of networked filesystems),
455 \li the name of the application which created it (returned in \a appname),
456 \endlist
457
458 Note that tryLock() automatically deleted the file if there is no
459 running application with this PID, so LockFailedError can only happen if there is
460 an application with this PID (it could be unrelated though).
461
462 This can be used to inform users about the existing lock file and give them
463 the choice to delete it. After removing the file using removeStaleLockFile(),
464 the application can call tryLock() again.
465
466 This function returns \c true if the information could be successfully retrieved, false
467 if the lock file doesn't exist or doesn't contain the expected data.
468 This can happen if the lock file was deleted between the time where tryLock() failed
469 and the call to this function. Simply call tryLock() again if this happens.
470*/
471bool QLockFile::getLockInfo(qint64 *pid, QString *hostname, QString *appname) const
472{
473 Q_D(const QLockFile);
474 std::optional opt = QLockFilePrivate::getLockInfo_helper(d->fileName);
475 if (!opt)
476 return false;
477 QLockFilePrivate::LockFileInfo &info = *opt;
478 if (pid)
479 *pid = info.pid;
480 if (hostname)
481 *hostname = info.hostname;
482 if (appname)
483 *appname = info.appname;
484 return true;
485}
486
487QLockFilePrivate::QLockFilePrivate(const QString &fn)
488 : fileName(fn)
489{
490}
491
492QLockFilePrivate::~QLockFilePrivate()
493 = default;
494
495QByteArray QLockFilePrivate::LockFileInfo::asFileContents() const
496{
497 // Use operator% from the fast builder to avoid multiple memory allocations.
498 return QByteArray::number(pid) % '\n'
499 % appname.toUtf8() % '\n'
500 % hostname.toUtf8() % '\n'
501 % hostid % '\n'
502 % bootid % '\n';
503}
504
505QLockFilePrivate::LockFileInfo::LockFileInfo(Current)
506 : pid(QCoreApplication::applicationPid()),
507 appname(processNameByPid(pid)),
508 hostname(machineName()),
509 hostid(QSysInfo::machineUniqueId()),
510 bootid(QSysInfo::bootUniqueId())
511{
512}
513
514static std::optional<QLockFilePrivate::LockFileInfo> getLockInfo(int fd)
515{
516 std::optional<QLockFilePrivate::LockFileInfo> info;
517 QFile reader;
518 if (!reader.open(fd, QFile::ReadOnly | QFile::Text, QFile::AutoCloseHandle)) {
519 QT_CLOSE(fd);
520 return info;
521 }
522
523 bool ok;
524 QByteArray pidLine = reader.readLine();
525 pidLine.chop(1);
526 qint64 pid = pidLine.toLongLong(&ok);
527 if (!ok || pid <= 0)
528 return info;
529 QByteArray appNameLine = reader.readLine();
530 appNameLine.chop(1);
531 QByteArray hostNameLine = reader.readLine();
532 hostNameLine.chop(1);
533
534 // prior to Qt 5.10, only the lines above were recorded
535 QByteArray hostId = reader.readLine();
536 hostId.chop(1);
537 QByteArray bootId = reader.readLine();
538 bootId.chop(1);
539
540 info.emplace();
541 info->appname = QString::fromUtf8(appNameLine);
542 info->hostname = QString::fromUtf8(hostNameLine);
543 info->hostid = std::move(hostId);
544 info->bootid = std::move(bootId);
545 info->pid = pid;
546 return info;
547}
548
549std::optional<QLockFilePrivate::LockFileInfo> QLockFilePrivate::getLockInfo_helper(const QString &fileName)
550{
551 int fd = openNewFileDescriptor(fileName);
552 return fd < 0 ? std::nullopt : getLockInfo(fd);
553}
554
555bool QLockFilePrivate::isApparentlyStale(const LockFileInfo &current) const
556{
557 QUniqueFileDescriptorHandle fd(openNewFileDescriptor(fileName));
558 if (!fd.isValid())
559 return false; // file has disappeared (or become inaccessible)
560
561 // check the file's mtime first (cheaper)
562 using namespace std::chrono;
563 if (staleLockTime > 0ms) {
564 QFileSystemMetaData md;
565 std::ignore = QFileSystemEngine::fillMetaData(fd.get(), md);
566 const QDateTime lastMod = md.fileTime(QFile::FileModificationTime);
567 const milliseconds age{lastMod.msecsTo(QDateTime::currentDateTimeUtc())};
568 if (abs(age) > staleLockTime)
569 return true;
570 }
571
572 if (std::optional opt = getLockInfo(fd.release())) {
573 LockFileInfo &info = *opt;
574 bool sameHost = info.hostname.isEmpty() || info.hostname == current.hostname;
575 if (!info.hostid.isEmpty()) {
576 // Override with the host ID, if we know it.
577 if (!current.hostid.isEmpty())
578 sameHost = (current.hostid == info.hostid);
579 }
580
581 if (sameHost) {
582 if (!info.bootid.isEmpty()) {
583 // If we've rebooted, then the lock is definitely stale.
584 if (info.bootid != current.bootid)
585 return true;
586 }
587 if (!isProcessRunning(info.pid, info.appname))
588 return true;
589 }
590 }
591
592 // not stale
593 return false;
594}
595
596/*!
597 Attempts to forcefully remove an existing lock file.
598
599 Calling this is not recommended when protecting a short-lived operation: QLockFile
600 already takes care of removing lock files after they are older than staleLockTime().
601
602 This method should only be called when protecting a resource for a long time, i.e.
603 with staleLockTime(0), and after tryLock() returned LockFailedError, and the user
604 agreed on removing the lock file.
605
606 Returns \c true on success, false if the lock file couldn't be removed. This happens
607 on Windows, when the application owning the lock is still running.
608*/
609bool QLockFile::removeStaleLockFile()
610{
611 Q_D(QLockFile);
612 if (d->isLocked) {
613 qWarning("removeStaleLockFile can only be called when not holding the lock");
614 return false;
615 }
616 return d->removeStaleLock();
617}
618
619/*!
620 Returns the lock file error status.
621
622 If tryLock() returns \c false, this function can be called to find out
623 the reason why the locking failed.
624*/
625QLockFile::LockError QLockFile::error() const
626{
627 Q_D(const QLockFile);
628 return d->lockError;
629}
630
631QT_END_NAMESPACE
Combined button and popup list for selecting options.
static std::optional< QLockFilePrivate::LockFileInfo > getLockInfo(int fd)
static QString machineName()
Definition qlockfile.cpp:30