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

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