Revision control

++ // Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
++ // Copyright (C) 2016 The Qt Company Ltd.
++ // Copyright (C) 2017 Intel Corporation.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "qlockfile.h"
++ #include "qlockfile_p.h"
++
++ #include <QtCore/qcoreapplication.h>
++ #include <QtCore/qdeadlinetimer.h>
++ #include <QtCore/qdatetime.h>
++ #include <QtCore/qfileinfo.h>
++ #include <QtCore/private/qfilesystemengine_p.h>
++ #include <QtCore/qrandom.h>
++ #include <QtCore/qthread.h>
++ #include <QtCore/private/quniquehandle_types_p.h>
++
++ #include <qplatformdefs.h>
++
++ #ifdef Q_OS_WIN
++ #include <io.h>
++ #include <qt_windows.h>
++ #endif
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ static QString machineName()
++ {
++ #ifdef Q_OS_WIN
++ // we don't use QSysInfo because it tries to do name resolution
++ return qEnvironmentVariable("COMPUTERNAME");
++ #else
++ return QSysInfo::machineHostName();
++ #endif
++ }
++
++ /*!
++ \class QLockFile
++ \inmodule QtCore
++ \ingroup io
++ \brief The QLockFile class provides locking between processes using a file.
++ \since 5.1
++
++ A lock file can be used to prevent multiple processes from accessing concurrently
++ the same resource. For instance, a configuration file on disk, or a socket, a port,
++ a region of shared memory...
++
++ Serialization is only guaranteed if all processes that access the shared resource
++ use QLockFile, with the same file path.
++
++ QLockFile supports two use cases:
++ to protect a resource for a short-term operation (e.g. verifying if a configuration
++ file has changed before saving new settings), and for long-lived protection of a
++ resource (e.g. a document opened by a user in an editor) for an indefinite amount of time.
++
++ When protecting for a short-term operation, it is acceptable to call lock() and wait
++ until any running operation finishes.
++ When protecting a resource over a long time, however, the application should always
++ call setStaleLockTime(0ms) and then tryLock() with a short timeout, in order to
++ warn the user that the resource is locked.
++
++ If the process holding the lock crashes, the lock file stays on disk and can prevent
++ any other process from accessing the shared resource, ever. For this reason, QLockFile
++ tries to detect such a "stale" lock file, based on the process ID written into the file.
++ To cover the situation that the process ID got reused meanwhile, the current process name is
++ compared to the name of the process that corresponds to the process ID from the lock file.
++ If the process names differ, the lock file is considered stale.
++ Additionally, the last modification time of the lock file (30s by default, for the use case of a
++ short-lived operation) is taken into account.
++ If the lock file is found to be stale, it will be deleted.
++
++ For the use case of protecting a resource over a long time, you should therefore call
++ setStaleLockTime(0), and when tryLock() returns LockFailedError, inform the user
++ that the document is locked, possibly using getLockInfo() for more details.
++
++ \section1 Limitations
++
++ QLockFile's implementation is usually safe, for the majority of
++ environments and filesystems. There are a few situations in which it can
++ incorrectly conclude a lock file is stale when it isn't, fail to detect a
++ stale file, or race when locking. This section documents those issues.
++
++ There are two main mitigation strategies: choosing a suitable, non-zero
++ staleLockTime() and choosing a regular, local filesystem for storing lock
++ files (such as \c{/var/lock} if running as root on Unix systems or the
++ \l{QStandardPaths::RuntimeLocation}{runtime location}). That may not be
++ possible for certain use-cases of QLockFile, such as when using QLockFile
++ to indicate a file path provided by the user is being edited.
++
++ \section2 Non-persistent machine IDs
++
++ QLockFile uses the machine's unique ID to differentiate a lock by the
++ current machine and one by a process running on a different host. If the
++ machine ID changes over time (such as on systems with ephemeral storage,
++ which must generate a new ID at every boot), QLockFile will be unable to
++ detect that a lock file is stale after a reboot.
++
++ Conversely, if two different machines have colliding machine IDs (for
++ example, a cloned system image or restoration from backups) and provide a
++ network path to QLockFile, the class may conclude the lock is stale when it
++ actually is not.
++
++ \section2 Absence of native locking
++
++ QLockFile uses operating-system specific calls to indicate to other
++ processes and threads that the lock is alive (not stale), even past the
++ staleLockTime() setting. This functionality may be absent for files on some
++ networked or virtual filesystems (such as FUSE on Linux and \macos), when
++ using different filesystems to access the same file if the native lock
++ isn't carried through, or in certain environments that block the necessary
++ system calls.
++
++ If the native locking support is absent, QLockFile will rely solely on the
++ existence, modification time, and contents of the lock file itself. In this
++ case, if the process currently locking the resource keeps it past
++ staleLockTime(), QLockFile will steal the lock.
++
++ \section2 Networked filesystems
++
++ It is unspecified whether the native file-locking is supported in networked
++ environments: with some implementations, it is supported for all clients
++ accessing the filesystem; for others the local system may support locking
++ for its own processes but will not share the locking over the network; and
++ for yet others there is no native locking even inside one host. Moreover,
++ it is possible that some clients participate in networked locking and some
++ others do not, for the same file. If locking is not supported across the
++ network, QLockFile will observe the limitations described above for the
++ absence of native locking.
++
++ Additionally, if the lock file contains the identification of a different
++ host, QLockFile will be unable to confirm the process holding a lock is
++ still running, and will need to wait staleLockTime() to recover from an
++ unclean exit.
++
++ \section2 Accessing different actual files through the same path
++
++ It is possible for two processes to have different views of the filesystem,
++ causing an identical file path to be different files in the filesystem. In
++ this case, the two QLockFile objects will likely succeed at creating the
++ lock, but will not be mutually exclusive. This may cause conflicts if the
++ resource the lock file is protecting is still shared between them.
++
++ This situation is most often encountered with containers (see below), but
++ is not exclusive to them.
++
++ \section2 Files shared with containers
++
++ With some container implementations, it is possible to hide the existence
++ of some processes (for example, Linux's "PID namespace" feature). If a
++ process is running inside of such a container but shares the machine ID of
++ the host system or another container, two processes in different containers
++ (or the host) will make incorrect determinations on whether the locking
++ process is still running. Moreover, some container controllers may replace
++ the boot ID inside of the container, causing the launched processes to
++ conclude the lock file is always stale, regardless of how fresh its
++ timestamp is.
++
++ With some other implementations, containers may have ephemeral storage or
++ intentionally create a new machine ID to avoid collision. In this case, the
++ application will experience the problems described above for non-persistent
++ machine IDs.
++
++ However, the native file-locking usually works (subject to filesystem and
++ environment limitations as discussed above), so even if QLockFile did
++ conclude the file is apparently stale, it won't steal a lock file that is
++ natively locked.
++
++ \section2 Accesses not using the same protocol
++
++ QLockFile cannot interoperate with modifications to the lock file performed
++ outside of the protocol implemented by this class. This includes removal of
++ the lock file by other tools, such as tmpwatch and similar, but also some
++ virus-scanning or similar tools.
++
++ \section2 Clock skew
++
++ QLockFile relies on the time stamp of the lock file being accurate. If the
++ clock jumps forward, QLockFile may conclude a lock file has become stale
++ when it hasn't, and vice-versa for jumping backwards.
++
++ For this reason, it is recommended all systems keep network-synchronized
++ time and perform this synchronization early in their boot process. This is
++ particularly important for networked filesystems.
++ */
++
++ /*!
++ \enum QLockFile::LockError
++
++ This enum describes the result of the last call to lock() or tryLock().
++
++ \value NoError The lock was acquired successfully.
++ \value LockFailedError The lock could not be acquired because another process holds it.
++ \value PermissionError The lock file could not be created, for lack of permissions
++ in the parent directory.
++ \value UnknownError Another error happened, for instance a full partition
++ prevented writing out the lock file.
++ */
++
++ /*!
++ Constructs a new lock file object.
++ The object is created in an unlocked state.
++ When calling lock() or tryLock(), a lock file named \a fileName will be created,
++ if it doesn't already exist.
++
++ \sa lock(), unlock()
++ */
++ QLockFile::QLockFile(const QString &fileName)
++ : d_ptr(new QLockFilePrivate(fileName))
++ {
++ }
++
++ /*!
++ Destroys the lock file object.
++ If the lock was acquired, this will release the lock, by deleting the lock file.
++ */
++ QLockFile::~QLockFile()
++ {
++ unlock();
++ }
++
++ /*!
++ * Returns the file name of the lock file
++ */
++ QString QLockFile::fileName() const
++ {
++ return d_ptr->fileName;
++ }
++
++ /*!
++ \fn void QLockFile::setStaleLockTime(int staleLockTime)
++
++ Sets \a staleLockTime to be the time in milliseconds after which
++ a lock file is considered stale.
++ The default value is 30000, i.e. 30 seconds.
++ If your application typically keeps the file locked for more than 30 seconds
++ (for instance while saving megabytes of data for 2 minutes), you should set
++ a bigger value using setStaleLockTime().
++
++ The value of \a staleLockTime is used by lock() and tryLock() in order
++ to determine when an existing lock file is considered stale, i.e. left over
++ by a crashed process. This is useful for the case where the PID got reused
++ meanwhile, so one way to detect a stale lock file is by the fact that
++ it has been around for a long time.
++
++ This is an overloaded function, equivalent to calling:
++ \code
++ setStaleLockTime(std::chrono::milliseconds{staleLockTime});
++ \endcode
++
++ \sa staleLockTime()
++ */
++
++ /*!
++ \since 6.2
++
++ Sets the interval after which a lock file is considered stale to \a staleLockTime.
++ The default value is 30s.
++
++ If your application typically keeps the file locked for more than 30 seconds
++ (for instance while saving megabytes of data for 2 minutes), you should set
++ a bigger value using setStaleLockTime().
++
++ The value of staleLockTime() is used by lock() and tryLock() in order
++ to determine when an existing lock file is considered stale, i.e. left over
++ by a crashed process. This is useful for the case where the PID got reused
++ meanwhile, so one way to detect a stale lock file is by the fact that
++ it has been around for a long time.
++
++ Setting this value to 0 or negative will disable the verification of
++ timestamps on lock files. QLockFile will still detect a stale lock if its
++ contents show that the locking process is no longer running.
++
++ \sa staleLockTime()
++ */
++ void QLockFile::setStaleLockTime(std::chrono::milliseconds staleLockTime)
++ {
++ Q_D(QLockFile);
++ d->staleLockTime = staleLockTime;
++ }
++
++ /*!
++ \fn int QLockFile::staleLockTime() const
++
++ Returns the time in milliseconds after which
++ a lock file is considered stale.
++
++ \sa setStaleLockTime()
++ */
++
++ /*! \fn std::chrono::milliseconds QLockFile::staleLockTimeAsDuration() const
++ \overload
++ \since 6.2
++
++ Returns a std::chrono::milliseconds object which denotes the time after
++ which a lock file is considered stale.
++
++ \sa setStaleLockTime()
++ */
++ std::chrono::milliseconds QLockFile::staleLockTimeAsDuration() const
++ {
++ Q_D(const QLockFile);
++ return d->staleLockTime;
++ }
++
++ /*!
++ Returns \c true if the lock was acquired by this QLockFile instance,
++ otherwise returns \c false.
++
++ \sa lock(), unlock(), tryLock()
++ */
++ bool QLockFile::isLocked() const
++ {
++ Q_D(const QLockFile);
++ return d->isLocked;
++ }
++
++ /*!
++ Creates the lock file.
++
++ If another process (or another thread) has created the lock file already,
++ this function will block until that process (or thread) releases it.
++
++ Calling this function multiple times on the same lock from the same
++ thread without unlocking first is not allowed. This function will
++ \e dead-lock when the file is locked recursively.
++
++ Returns \c true if the lock was acquired, false if it could not be acquired
++ due to an unrecoverable error, such as no permissions in the parent directory.
++
++ \sa unlock(), tryLock()
++ */
++ bool QLockFile::lock()
++ {
++ return tryLock(std::chrono::milliseconds::max());
++ }
++
++ /*!
++ \fn bool QLockFile::tryLock(int timeout)
++
++ Attempts to create the lock file. This function returns \c true if the
++ lock was obtained; otherwise it returns \c false. If another process (or
++ another thread) has created the lock file already, this function will
++ wait for at most \a timeout milliseconds for the lock file to become
++ available.
++
++ Note: Passing a negative number as the \a timeout is equivalent to
++ calling lock(), i.e. this function will wait forever until the lock
++ file can be locked if \a timeout is negative.
++
++ If the lock was obtained, it must be released with unlock()
++ before another process (or thread) can successfully lock it.
++
++ Calling this function multiple times on the same lock from the same
++ thread without unlocking first is not allowed, this function will
++ \e always return false when attempting to lock the file recursively.
++
++ \sa lock(), unlock()
++ */
++
++ /*!
++ \overload
++ \since 6.2
++
++ Attempts to create the lock file. This function returns \c true if the
++ lock was obtained; otherwise it returns \c false. If another process (or
++ another thread) has created the lock file already, this function will
++ wait for at most \a timeout for the lock file to become available.
++
++ If the lock was obtained, it must be released with unlock()
++ before another process (or thread) can successfully lock it.
++
++ Calling this function multiple times on the same lock from the same
++ thread without unlocking first is not allowed, this function will
++ \e always return false when attempting to lock the file recursively.
++
++ \sa lock(), unlock()
++ */
++ bool QLockFile::tryLock(std::chrono::milliseconds timeout)
++ {
++ using namespace std::chrono_literals;
++ constexpr int Perturbation = 4096 * 1024; // ns
++ using Msec = std::chrono::milliseconds;
++
++ Q_D(QLockFile);
++ QLockFilePrivate::LockFileInfo current(QLockFilePrivate::LockFileInfo::Current{});
++
++ // Don't convert \a timeout to a different duration type!
++ QDeadlineTimer timer(timeout < 0ms ? Msec::max() : timeout);
++
++ // Add a small perturbation to the sleep time, just in case two or more
++ // processes/threads are getting woken up together.
++ auto sleepTime = 100ms +
++ QRandomGenerator::global()->bounded(-Perturbation, Perturbation) * 1ns;
++
++ while (true) {
++ d->lockError = d->tryLock_sys(current);
++ switch (d->lockError) {
++ case NoError:
++ d->isLocked = true;
++ return true;
++ case PermissionError:
++ case UnknownError:
++ return false;
++ case LockFailedError:
++ if (!d->isLocked && d->isApparentlyStale(current)) {
++ if (Q_UNLIKELY(QFileInfo(d->fileName).lastModified(QTimeZone::UTC) > QDateTime::currentDateTimeUtc()))
++ qInfo("QLockFile: Lock file '%ls' has a modification time in the future", qUtf16Printable(d->fileName));
++ // Stale lock from another thread/process
++ // Ensure two processes don't remove it at the same time
++ QLockFile rmlock(d->fileName + ".rmlock"_L1);
++ if (rmlock.tryLock()) {
++ if (d->isApparentlyStale(current) && d->removeStaleLock())
++ continue;
++ }
++ }
++ break;
++ }
++
++ auto remainingTime = timer.remainingTimeAsDuration();
++ if (remainingTime == 0ms)
++ return false;
++
++ if (sleepTime > remainingTime)
++ sleepTime = remainingTime;
++
++ QThread::sleep(sleepTime);
++ if (sleepTime < 5s)
++ sleepTime *= 2;
++ }
++ // not reached
++ return false;
++ }
++
++ /*!
++ \fn void QLockFile::unlock()
++ Releases the lock, by deleting the lock file.
++
++ Calling unlock() without locking the file first, does nothing.
++
++ \sa lock(), tryLock()
++ */
++
++ /*!
++ Retrieves information about the current owner of the lock file.
++
++ If tryLock() returns \c false, and error() returns LockFailedError,
++ this function can be called to find out more information about the existing
++ lock file:
++ \list
++ \li the PID of the application (returned in \a pid)
++ \li the \a hostname it's running on (useful in case of networked filesystems),
++ \li the name of the application which created it (returned in \a appname),
++ \endlist
++
++ Note that tryLock() automatically deleted the file if there is no
++ running application with this PID, so LockFailedError can only happen if there is
++ an application with this PID (it could be unrelated though).
++
++ This can be used to inform users about the existing lock file and give them
++ the choice to delete it. After removing the file using removeStaleLockFile(),
++ the application can call tryLock() again.
++
++ This function returns \c true if the information could be successfully retrieved, false
++ if the lock file doesn't exist or doesn't contain the expected data.
++ This can happen if the lock file was deleted between the time where tryLock() failed
++ and the call to this function. Simply call tryLock() again if this happens.
++ */
++ bool QLockFile::getLockInfo(qint64 *pid, QString *hostname, QString *appname) const
++ {
++ Q_D(const QLockFile);
++ std::optional opt = QLockFilePrivate::getLockInfo_helper(d->fileName);
++ if (!opt)
++ return false;
++ QLockFilePrivate::LockFileInfo &info = *opt;
++ if (pid)
++ *pid = info.pid;
++ if (hostname)
++ *hostname = info.hostname;
++ if (appname)
++ *appname = info.appname;
++ return true;
++ }
++
++ QLockFilePrivate::QLockFilePrivate(const QString &fn)
++ : fileName(fn)
++ {
++ }
++
++ QLockFilePrivate::~QLockFilePrivate()
++ = default;
++
++ QByteArray QLockFilePrivate::LockFileInfo::asFileContents() const
++ {
++ // Use operator% from the fast builder to avoid multiple memory allocations.
++ return QByteArray::number(pid) % '\n'
++ % appname.toUtf8() % '\n'
++ % hostname.toUtf8() % '\n'
++ % hostid % '\n'
++ % bootid % '\n';
++ }
++
++ QLockFilePrivate::LockFileInfo::LockFileInfo(Current)
++ : pid(QCoreApplication::applicationPid()),
++ appname(processNameByPid(pid)),
++ hostname(machineName()),
++ hostid(QSysInfo::machineUniqueId()),
++ bootid(QSysInfo::bootUniqueId())
++ {
++ }
++
++ static std::optional<QLockFilePrivate::LockFileInfo> getLockInfo(int fd)
++ {
++ std::optional<QLockFilePrivate::LockFileInfo> info;
++ QFile reader;
++ if (!reader.open(fd, QFile::ReadOnly | QFile::Text, QFile::AutoCloseHandle)) {
++ QT_CLOSE(fd);
++ return info;
++ }
++
++ bool ok;
++ QByteArray pidLine = reader.readLine();
++ pidLine.chop(1);
++ qint64 pid = pidLine.toLongLong(&ok);
++ if (!ok || pid <= 0)
++ return info;
++ QByteArray appNameLine = reader.readLine();
++ appNameLine.chop(1);
++ QByteArray hostNameLine = reader.readLine();
++ hostNameLine.chop(1);
++
++ // prior to Qt 5.10, only the lines above were recorded
++ QByteArray hostId = reader.readLine();
++ hostId.chop(1);
++ QByteArray bootId = reader.readLine();
++ bootId.chop(1);
++
++ info.emplace();
++ info->appname = QString::fromUtf8(appNameLine);
++ info->hostname = QString::fromUtf8(hostNameLine);
++ info->hostid = std::move(hostId);
++ info->bootid = std::move(bootId);
++ info->pid = pid;
++ return info;
++ }
++
++ std::optional<QLockFilePrivate::LockFileInfo> QLockFilePrivate::getLockInfo_helper(const QString &fileName)
++ {
++ int fd = openNewFileDescriptor(fileName);
++ return fd < 0 ? std::nullopt : getLockInfo(fd);
++ }
++
++ bool QLockFilePrivate::isApparentlyStale(const LockFileInfo &current) const
++ {
++ QUniqueFileDescriptorHandle fd(openNewFileDescriptor(fileName));
++ if (!fd.isValid())
++ return false; // file has disappeared (or become inaccessible)
++
++ // check the file's mtime first (cheaper)
++ using namespace std::chrono;
++ if (staleLockTime > 0ms) {
++ QFileSystemMetaData md;
++ std::ignore = QFileSystemEngine::fillMetaData(fd.get(), md);
++ const QDateTime lastMod = md.fileTime(QFile::FileModificationTime);
++ const milliseconds age{lastMod.msecsTo(QDateTime::currentDateTimeUtc())};
++ if (abs(age) > staleLockTime)
++ return true;
++ }
++
++ if (std::optional opt = getLockInfo(fd.release())) {
++ LockFileInfo &info = *opt;
++ bool sameHost = info.hostname.isEmpty() || info.hostname == current.hostname;
++ if (!info.hostid.isEmpty()) {
++ // Override with the host ID, if we know it.
++ if (!current.hostid.isEmpty())
++ sameHost = (current.hostid == info.hostid);
++ }
++
++ if (sameHost) {
++ if (!info.bootid.isEmpty()) {
++ // If we've rebooted, then the lock is definitely stale.
++ if (info.bootid != current.bootid)
++ return true;
++ }
++ if (!isProcessRunning(info.pid, info.appname))
++ return true;
++ }
++ }
++
++ // not stale
++ return false;
++ }
++
++ /*!
++ Attempts to forcefully remove an existing lock file.
++
++ Calling this is not recommended when protecting a short-lived operation: QLockFile
++ already takes care of removing lock files after they are older than staleLockTime().
++
++ This method should only be called when protecting a resource for a long time, i.e.
++ with staleLockTime(0), and after tryLock() returned LockFailedError, and the user
++ agreed on removing the lock file.
++
++ Returns \c true on success, false if the lock file couldn't be removed. This happens
++ on Windows, when the application owning the lock is still running.
++ */
++ bool QLockFile::removeStaleLockFile()
++ {
++ Q_D(QLockFile);
++ if (d->isLocked) {
++ qWarning("removeStaleLockFile can only be called when not holding the lock");
++ return false;
++ }
++ return d->removeStaleLock();
++ }
++
++ /*!
++ Returns the lock file error status.
++
++ If tryLock() returns \c false, this function can be called to find out
++ the reason why the locking failed.
++ */
++ QLockFile::LockError QLockFile::error() const
++ {
++ Q_D(const QLockFile);
++ return d->lockError;
++ }
++
++ QT_END_NAMESPACE