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