Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Toggle main menu visibility
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
26
QT_BEGIN_NAMESPACE
27
28
using
namespace
Qt::StringLiterals;
29
30
static
QString
machineName
()
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
*/
210
QLockFile::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
*/
219
QLockFile::~QLockFile()
220
{
221
unlock();
222
}
223
224
/*!
225
* Returns the file name of the lock file
226
*/
227
QString 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
*/
278
void
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
*/
302
std::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
*/
314
bool
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
*/
335
bool
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
*/
381
bool
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
*/
471
bool
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
487
QLockFilePrivate::QLockFilePrivate(
const
QString &fn)
488
: fileName(fn)
489
{
490
}
491
492
QLockFilePrivate::~QLockFilePrivate()
493
=
default
;
494
495
QByteArray 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
505
QLockFilePrivate::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
514
static
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
549
std
::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
555
bool
QLockFilePrivate::isApparentlyStale(
const
LockFileInfo ¤t)
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
*/
609
bool
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
*/
625
QLockFile::LockError QLockFile::error()
const
626
{
627
Q_D(
const
QLockFile);
628
return
d->lockError;
629
}
630
631
QT_END_NAMESPACE
QT_BEGIN_NAMESPACE
Combined button and popup list for selecting options.
Definition
qsequentialanimationgroup.cpp:47
std
[33]
Definition
src_corelib_tools_qhash.cpp:421
getLockInfo
static std::optional< QLockFilePrivate::LockFileInfo > getLockInfo(int fd)
Definition
qlockfile.cpp:514
machineName
static QString machineName()
Definition
qlockfile.cpp:30
qtbase
src
corelib
io
qlockfile.cpp
Generated on
for Qt by
1.18.0