| 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 |  | 
| 15 | QT_BEGIN_NAMESPACE | 
| 16 |  | 
| 17 | using namespace Qt::StringLiterals; | 
| 18 |  | 
| 19 | namespace { | 
| 20 | struct LockFileInfo | 
| 21 | { | 
| 22 |     qint64 pid; | 
| 23 |     QString appname; | 
| 24 |     QString hostname; | 
| 25 |     QByteArray hostid; | 
| 26 |     QByteArray bootid; | 
| 27 | }; | 
| 28 | } | 
| 29 |  | 
| 30 | static bool getLockInfo_helper(const QString &fileName, LockFileInfo *info); | 
| 31 |  | 
| 32 | static 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 | */ | 
| 106 | QLockFile::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 | */ | 
| 115 | QLockFile::~QLockFile() | 
| 116 | { | 
| 117 |     unlock(); | 
| 118 | } | 
| 119 |  | 
| 120 | /*! | 
| 121 |  * Returns the file name of the lock file | 
| 122 |  */ | 
| 123 | QString 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 | */ | 
| 149 | void 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 | */ | 
| 172 | void 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 | */ | 
| 184 | int 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 | */ | 
| 198 | std::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 | */ | 
| 210 | bool 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 | */ | 
| 231 | bool 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 | */ | 
| 256 | bool 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 | */ | 
| 279 | bool 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 | */ | 
| 362 | bool 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 |  | 
| 377 | QByteArray 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 |  | 
| 387 | static 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 |  | 
| 417 | bool 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 | */ | 
| 459 | bool 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 | */ | 
| 475 | QLockFile::LockError QLockFile::error() const | 
| 476 | { | 
| 477 |     Q_D(const QLockFile); | 
| 478 |     return d->lockError; | 
| 479 | } | 
| 480 |  | 
| 481 | QT_END_NAMESPACE | 
| 482 |  |