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

source code of qtbase/src/corelib/io/qlockfile.cpp