From 1b582d64eb6d13e60a02ebc40956302a4864eb6c Mon Sep 17 00:00:00 2001 From: David Faure Date: Sun, 3 Feb 2013 12:00:50 +0100 Subject: [PATCH] Long live QLockFile Locking between processes, implemented with open(O_EXCL) on Unix and CreateFile(CREATE_NEW) on Windows. Supports detecting stale lock files and deleting them. Advisory locking is used to prevent deletion of files that are still in use. Change-Id: Id00ee2a4e77a29483d869037c7047c59cb909339 Reviewed-by: Thiago Macieira --- .gitignore | 1 + src/corelib/io/io.pri | 5 + src/corelib/io/qlockfile.cpp | 346 ++++++++++++++++ src/corelib/io/qlockfile.h | 91 +++++ src/corelib/io/qlockfile_p.h | 104 +++++ src/corelib/io/qlockfile_unix.cpp | 207 ++++++++++ src/corelib/io/qlockfile_win.cpp | 138 +++++++ src/corelib/io/qtemporaryfile.h | 2 + src/corelib/io/qtemporaryfile_p.h | 2 + tests/auto/corelib/io/io.pro | 1 + tests/auto/corelib/io/qlockfile/qlockfile.pro | 3 + .../qlockfile_test_helper.cpp | 78 ++++ .../qlockfile_test_helper.pro | 7 + .../corelib/io/qlockfile/tst_qlockfile.cpp | 379 ++++++++++++++++++ .../corelib/io/qlockfile/tst_qlockfile.pro | 6 + 15 files changed, 1370 insertions(+) create mode 100644 src/corelib/io/qlockfile.cpp create mode 100644 src/corelib/io/qlockfile.h create mode 100644 src/corelib/io/qlockfile_p.h create mode 100644 src/corelib/io/qlockfile_unix.cpp create mode 100644 src/corelib/io/qlockfile_win.cpp create mode 100644 tests/auto/corelib/io/qlockfile/qlockfile.pro create mode 100644 tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper.cpp create mode 100644 tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper.pro create mode 100644 tests/auto/corelib/io/qlockfile/tst_qlockfile.cpp create mode 100644 tests/auto/corelib/io/qlockfile/tst_qlockfile.pro diff --git a/.gitignore b/.gitignore index a64b0ccf9a..5f9854a674 100644 --- a/.gitignore +++ b/.gitignore @@ -307,6 +307,7 @@ tests/auto/corelib/thread/qthreadstorage/crashOnExit tests/auto/corelib/io/qresourceengine/qresourceengine tests/auto/corelib/codecs/qtextcodec/echo/echo tests/auto/corelib/plugin/quuid/testProcessUniqueness/testProcessUniqueness +tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper tests/auto/dbus/qdbusabstractadaptor/qmyserver/qmyserver tests/auto/dbus/qdbusabstractinterface/qpinger/qpinger tests/auto/dbus/qdbusinterface/qmyserver/qmyserver diff --git a/src/corelib/io/io.pri b/src/corelib/io/io.pri index e0364a1460..d4ed8e5362 100644 --- a/src/corelib/io/io.pri +++ b/src/corelib/io/io.pri @@ -18,6 +18,8 @@ HEADERS += \ io/qipaddress_p.h \ io/qiodevice.h \ io/qiodevice_p.h \ + io/qlockfile.h \ + io/qlockfile_p.h \ io/qnoncontiguousbytedevice_p.h \ io/qprocess.h \ io/qprocess_p.h \ @@ -61,6 +63,7 @@ SOURCES += \ io/qfileinfo.cpp \ io/qipaddress.cpp \ io/qiodevice.cpp \ + io/qlockfile.cpp \ io/qnoncontiguousbytedevice.cpp \ io/qprocess.cpp \ io/qtextstream.cpp \ @@ -85,6 +88,7 @@ SOURCES += \ win32 { SOURCES += io/qsettings_win.cpp SOURCES += io/qfsfileengine_win.cpp + SOURCES += io/qlockfile_win.cpp SOURCES += io/qfilesystemwatcher_win.cpp HEADERS += io/qfilesystemwatcher_win_p.h @@ -109,6 +113,7 @@ win32 { SOURCES += \ io/qfsfileengine_unix.cpp \ io/qfilesystemengine_unix.cpp \ + io/qlockfile_unix.cpp \ io/qprocess_unix.cpp \ io/qfilesystemiterator_unix.cpp \ diff --git a/src/corelib/io/qlockfile.cpp b/src/corelib/io/qlockfile.cpp new file mode 100644 index 0000000000..5d56a67f48 --- /dev/null +++ b/src/corelib/io/qlockfile.cpp @@ -0,0 +1,346 @@ +/**************************************************************************** +** +** Copyright (C) 2013 David Faure +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qlockfile.h" +#include "qlockfile_p.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \class QLockFile + \inmodule QtCore + \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(0) 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, + and (in case that process ID got reused meanwhile), on the last modification time of + the lock file (30s by default, for the use case of a short-lived operation). + 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. +*/ + +/*! + \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(); +} + +/*! + 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 the only way to detect a stale lock file is by the fact that + it has been around for a long time. + + \sa staleLockTime() +*/ +void QLockFile::setStaleLockTime(int staleLockTime) +{ + Q_D(QLockFile); + d->staleLockTime = staleLockTime; +} + +/*! + Returns the time in milliseconds after which + a lock file is considered stale. + + \sa setStaleLockTime() +*/ +int QLockFile::staleLockTime() const +{ + Q_D(const QLockFile); + return d->staleLockTime; +} + +/*! + Returns true if the lock was acquired by this QLockFile instance, + otherwise returns 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 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(-1); +} + +/*! + Attempts to create the lock file. This function returns true if the + lock was obtained; otherwise it returns 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() +*/ +bool QLockFile::tryLock(int timeout) +{ + Q_D(QLockFile); + QElapsedTimer timer; + if (timeout > 0) + timer.start(); + int sleepTime = 100; + forever { + d->lockError = d->tryLock_sys(); + switch (d->lockError) { + case NoError: + d->isLocked = true; + return true; + case PermissionError: + case UnknownError: + return false; + case LockFailedError: + if (!d->isLocked && d->isApparentlyStale()) { + // Stale lock from another thread/process + // Ensure two processes don't remove it at the same time + QLockFile rmlock(d->fileName + QStringLiteral(".rmlock")); + if (rmlock.tryLock()) { + if (d->isApparentlyStale() && d->removeStaleLock()) + continue; + } + } + break; + } + if (timeout == 0 || (timeout > 0 && timer.hasExpired(timeout))) + return false; + QThread::msleep(sleepTime); + if (sleepTime < 5 * 1000) + 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 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 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); + return d->getLockInfo(pid, hostname, appname); +} + +bool QLockFilePrivate::getLockInfo(qint64 *pid, QString *hostname, QString *appname) const +{ + QFile reader(fileName); + if (!reader.open(QIODevice::ReadOnly)) + return false; + + QByteArray pidLine = reader.readLine(); + pidLine.chop(1); + QByteArray appNameLine = reader.readLine(); + appNameLine.chop(1); + QByteArray hostNameLine = reader.readLine(); + hostNameLine.chop(1); + if (pidLine.isEmpty() || appNameLine.isEmpty()) + return false; + + qint64 thePid = pidLine.toLongLong(); + if (pid) + *pid = thePid; + if (appname) + *appname = QString::fromUtf8(appNameLine); + if (hostname) + *hostname = QString::fromUtf8(hostNameLine); + return thePid > 0; +} + +/*! + 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 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 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 diff --git a/src/corelib/io/qlockfile.h b/src/corelib/io/qlockfile.h new file mode 100644 index 0000000000..4c8b6bf31a --- /dev/null +++ b/src/corelib/io/qlockfile.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2013 David Faure +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QLOCKFILE_H +#define QLOCKFILE_H + +#include +#include + +QT_BEGIN_HEADER + +QT_BEGIN_NAMESPACE + +class QLockFilePrivate; + +class Q_CORE_EXPORT QLockFile +{ +public: + QLockFile(const QString &fileName); + ~QLockFile(); + + bool lock(); + bool tryLock(int timeout = 0); + void unlock(); + + void setStaleLockTime(int); + int staleLockTime() const; + + bool isLocked() const; + bool getLockInfo(qint64 *pid, QString *hostname, QString *appname) const; + bool removeStaleLockFile(); + + enum LockError { + NoError = 0, + LockFailedError = 1, + PermissionError = 2, + UnknownError = 3 + }; + LockError error() const; + +protected: + QScopedPointer d_ptr; + +private: + Q_DECLARE_PRIVATE(QLockFile) + Q_DISABLE_COPY(QLockFile) +}; + +QT_END_NAMESPACE + +QT_END_HEADER + +#endif // QLOCKFILE_H diff --git a/src/corelib/io/qlockfile_p.h b/src/corelib/io/qlockfile_p.h new file mode 100644 index 0000000000..e046e87cf4 --- /dev/null +++ b/src/corelib/io/qlockfile_p.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2013 David Faure +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QLOCKFILE_P_H +#define QLOCKFILE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +#ifdef Q_OS_WIN +#include +#endif + +QT_BEGIN_NAMESPACE + +class QLockFilePrivate +{ +public: + QLockFilePrivate(const QString &fn) + : fileName(fn), +#ifdef Q_OS_WIN + fileHandle(INVALID_HANDLE_VALUE), +#else + fileHandle(-1), +#endif + staleLockTime(30 * 1000), // 30 seconds + lockError(QLockFile::NoError), + isLocked(false) + { + } + QLockFile::LockError tryLock_sys(); + bool removeStaleLock(); + bool getLockInfo(qint64 *pid, QString *hostname, QString *appname) const; + // Returns true if the lock belongs to dead PID, or is old. + // The attempt to delete it will tell us if it was really stale or not, though. + bool isApparentlyStale() const; + +#ifdef Q_OS_UNIX + static int checkFcntlWorksAfterFlock(); +#endif + + QString fileName; +#ifdef Q_OS_WIN + Qt::HANDLE fileHandle; +#else + int fileHandle; +#endif + int staleLockTime; // "int milliseconds" is big enough for 24 days + QLockFile::LockError lockError; + bool isLocked; +}; + +QT_END_NAMESPACE + +#endif /* QLOCKFILE_P_H */ diff --git a/src/corelib/io/qlockfile_unix.cpp b/src/corelib/io/qlockfile_unix.cpp new file mode 100644 index 0000000000..ed3b399fbf --- /dev/null +++ b/src/corelib/io/qlockfile_unix.cpp @@ -0,0 +1,207 @@ +/**************************************************************************** +** +** Copyright (C) 2013 David Faure +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "private/qlockfile_p.h" + +#include "QtCore/qtemporaryfile.h" +#include "QtCore/qcoreapplication.h" +#include "QtCore/qfileinfo.h" +#include "QtCore/qdebug.h" + +#include "private/qcore_unix_p.h" // qt_safe_open +#include "private/qabstractfileengine_p.h" +#include "private/qtemporaryfile_p.h" + +#include // flock +#include // kill +#include // kill + +QT_BEGIN_NAMESPACE + +static QString localHostName() // from QHostInfo::localHostName() +{ + char hostName[512]; + if (gethostname(hostName, sizeof(hostName)) == -1) + return QString(); + hostName[sizeof(hostName) - 1] = '\0'; + return QString::fromLocal8Bit(hostName); +} + +// ### merge into qt_safe_write? +static qint64 qt_write_loop(int fd, const char *data, qint64 len) +{ + qint64 pos = 0; + while (pos < len) { + const qint64 ret = qt_safe_write(fd, data + pos, len - pos); + if (ret == -1) // e.g. partition full + return pos; + pos += ret; + } + return pos; +} + +int QLockFilePrivate::checkFcntlWorksAfterFlock() +{ + QTemporaryFile file; + if (!file.open()) + return -2; + const int fd = file.d_func()->engine()->handle(); + if (flock(fd, LOCK_EX | LOCK_NB) == -1) // other threads, and other processes on a local fs + return -3; + struct flock flockData; + flockData.l_type = F_WRLCK; + flockData.l_whence = SEEK_SET; + flockData.l_start = 0; + flockData.l_len = 0; // 0 = entire file + flockData.l_pid = getpid(); + if (fcntl(fd, F_SETLK, &flockData) == -1) // for networked filesystems + return 0; + return 1; +} + +static QBasicAtomicInt fcntlOK = Q_BASIC_ATOMIC_INITIALIZER(-1); + +/*! + \internal + Checks that the OS isn't using POSIX locks to emulate flock(). + Mac OS X is one of those. +*/ +static bool fcntlWorksAfterFlock() +{ + int value = fcntlOK.load(); + if (Q_UNLIKELY(value == -1)) { + value = QLockFilePrivate::checkFcntlWorksAfterFlock(); + fcntlOK.store(value); + } + return value == 1; +} + +static bool setNativeLocks(int fd) +{ + if (flock(fd, LOCK_EX | LOCK_NB) == -1) // other threads, and other processes on a local fs + return false; + struct flock flockData; + flockData.l_type = F_WRLCK; + flockData.l_whence = SEEK_SET; + flockData.l_start = 0; + flockData.l_len = 0; // 0 = entire file + flockData.l_pid = getpid(); + if (fcntlWorksAfterFlock() && fcntl(fd, F_SETLK, &flockData) == -1) // for networked filesystems + return false; + return true; +} + +QLockFile::LockError QLockFilePrivate::tryLock_sys() +{ + const QByteArray lockFileName = QFile::encodeName(fileName); + const int fd = qt_safe_open(lockFileName.constData(), O_WRONLY | O_CREAT | O_EXCL, 0644); + if (fd < 0) { + switch (errno) { + case EEXIST: + return QLockFile::LockFailedError; + case EACCES: + case EROFS: + return QLockFile::PermissionError; + default: + return QLockFile::UnknownError; + } + } + // Ensure nobody else can delete the file while we have it + if (!setNativeLocks(fd)) + qWarning() << "setNativeLocks failed:" << strerror(errno); + + // We hold the lock, continue. + fileHandle = fd; + + // Assemble data, to write in a single call to write + // (otherwise we'd have to check every write call) + QByteArray fileData; + fileData += QByteArray::number(QCoreApplication::applicationPid()); + fileData += '\n'; + fileData += qAppName().toUtf8(); + fileData += '\n'; + fileData += localHostName().toUtf8(); + fileData += '\n'; + + QLockFile::LockError error = QLockFile::NoError; + if (qt_write_loop(fd, fileData.constData(), fileData.size()) < fileData.size()) + error = QLockFile::UnknownError; // partition full + return error; +} + +bool QLockFilePrivate::removeStaleLock() +{ + const QByteArray lockFileName = QFile::encodeName(fileName); + const int fd = qt_safe_open(lockFileName.constData(), O_WRONLY, 0644); + if (fd < 0) // gone already? + return false; + bool success = setNativeLocks(fd) && (::unlink(lockFileName) == 0); + close(fd); + return success; +} + +bool QLockFilePrivate::isApparentlyStale() const +{ + qint64 pid; + QString hostname, appname; + if (!getLockInfo(&pid, &hostname, &appname)) + return false; + if (hostname == localHostName()) { + if (::kill(pid, 0) == -1 && errno == ESRCH) + return true; // PID doesn't exist anymore + } + const qint64 age = QFileInfo(fileName).lastModified().msecsTo(QDateTime::currentDateTime()); + return staleLockTime > 0 && age > staleLockTime; +} + +void QLockFile::unlock() +{ + Q_D(QLockFile); + if (!d->isLocked) + return; + close(d->fileHandle); + d->fileHandle = -1; + QFile::remove(d->fileName); + d->lockError = QLockFile::NoError; + d->isLocked = false; +} + +QT_END_NAMESPACE diff --git a/src/corelib/io/qlockfile_win.cpp b/src/corelib/io/qlockfile_win.cpp new file mode 100644 index 0000000000..b5f6d9f3da --- /dev/null +++ b/src/corelib/io/qlockfile_win.cpp @@ -0,0 +1,138 @@ +/**************************************************************************** +** +** Copyright (C) 2013 David Faure +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "private/qlockfile_p.h" +#include "private/qfilesystementry_p.h" +#include + +#include "QtCore/qcoreapplication.h" +#include "QtCore/qfileinfo.h" +#include "QtCore/qdatetime.h" +#include "QtCore/qdebug.h" + +QT_BEGIN_NAMESPACE + +QLockFile::LockError QLockFilePrivate::tryLock_sys() +{ + SECURITY_ATTRIBUTES securityAtts = { sizeof(SECURITY_ATTRIBUTES), NULL, FALSE }; + const QFileSystemEntry fileEntry(fileName); + // When writing, allow others to read. + // When reading, QFile will allow others to read and write, all good. + // Adding FILE_SHARE_DELETE would allow forceful deletion of stale files, + // but Windows doesn't allow recreating it while this handle is open anyway, + // so this would only create confusion (can't lock, but no lock file to read from). + const DWORD dwShareMode = FILE_SHARE_READ; + HANDLE fh = CreateFile((const wchar_t*)fileEntry.nativeFilePath().utf16(), + GENERIC_WRITE, + dwShareMode, + &securityAtts, + CREATE_NEW, // error if already exists + FILE_ATTRIBUTE_NORMAL, + NULL); + if (fh == INVALID_HANDLE_VALUE) { + const DWORD lastError = GetLastError(); + switch (lastError) { + case ERROR_SHARING_VIOLATION: + case ERROR_ALREADY_EXISTS: + case ERROR_FILE_EXISTS: + case ERROR_ACCESS_DENIED: // readonly file, or file still in use by another process. Assume the latter, since we don't create it readonly. + return QLockFile::LockFailedError; + default: + qWarning() << "Got unexpected locking error" << lastError; + return QLockFile::UnknownError; + } + } + + // We hold the lock, continue. + fileHandle = fh; + // Assemble data, to write in a single call to write + // (otherwise we'd have to check every write call) + QByteArray fileData; + fileData += QByteArray::number(QCoreApplication::applicationPid()); + fileData += '\n'; + fileData += qAppName().toUtf8(); + fileData += '\n'; + //fileData += localHostname(); // gethostname requires winsock init, see QHostInfo... + fileData += '\n'; + DWORD bytesWritten = 0; + QLockFile::LockError error = QLockFile::NoError; + if (!WriteFile(fh, fileData.constData(), fileData.size(), &bytesWritten, NULL) || !FlushFileBuffers(fh)) + error = QLockFile::UnknownError; // partition full + return error; +} + +bool QLockFilePrivate::removeStaleLock() +{ + // QFile::remove fails on Windows if the other process is still using the file, so it's not stale. + return QFile::remove(fileName); +} + +bool QLockFilePrivate::isApparentlyStale() const +{ + qint64 pid; + QString hostname, appname; + if (!getLockInfo(&pid, &hostname, &appname)) + return false; + + HANDLE procHandle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); + if (!procHandle) + return true; + // We got a handle but check if process is still alive + DWORD dwR = ::WaitForSingleObject(procHandle, 0); + ::CloseHandle(procHandle); + if (dwR == WAIT_TIMEOUT) + return true; + const qint64 age = QFileInfo(fileName).lastModified().msecsTo(QDateTime::currentDateTime()); + return staleLockTime > 0 && age > staleLockTime; +} + +void QLockFile::unlock() +{ + Q_D(QLockFile); + if (!d->isLocked) + return; + CloseHandle(d->fileHandle); + QFile::remove(d->fileName); + d->lockError = QLockFile::NoError; + d->isLocked = false; +} + +QT_END_NAMESPACE diff --git a/src/corelib/io/qtemporaryfile.h b/src/corelib/io/qtemporaryfile.h index 249892e704..09aa53c33b 100644 --- a/src/corelib/io/qtemporaryfile.h +++ b/src/corelib/io/qtemporaryfile.h @@ -55,6 +55,7 @@ QT_BEGIN_NAMESPACE #ifndef QT_NO_TEMPORARYFILE class QTemporaryFilePrivate; +class QLockFilePrivate; class Q_CORE_EXPORT QTemporaryFile : public QFile { @@ -96,6 +97,7 @@ protected: private: friend class QFile; + friend class QLockFilePrivate; Q_DISABLE_COPY(QTemporaryFile) }; diff --git a/src/corelib/io/qtemporaryfile_p.h b/src/corelib/io/qtemporaryfile_p.h index dd011f56c1..d274f60ecc 100644 --- a/src/corelib/io/qtemporaryfile_p.h +++ b/src/corelib/io/qtemporaryfile_p.h @@ -62,6 +62,8 @@ protected: QString templateName; static QString defaultTemplateName(); + + friend class QLockFilePrivate; }; class QTemporaryFileEngine : public QFSFileEngine diff --git a/tests/auto/corelib/io/io.pro b/tests/auto/corelib/io/io.pro index 80ae6d38c1..b3a51c6f6e 100644 --- a/tests/auto/corelib/io/io.pro +++ b/tests/auto/corelib/io/io.pro @@ -14,6 +14,7 @@ SUBDIRS=\ qfilesystemwatcher \ qiodevice \ qipaddress \ + qlockfile \ qnodebug \ qprocess \ qprocess-noapplication \ diff --git a/tests/auto/corelib/io/qlockfile/qlockfile.pro b/tests/auto/corelib/io/qlockfile/qlockfile.pro new file mode 100644 index 0000000000..91f104305c --- /dev/null +++ b/tests/auto/corelib/io/qlockfile/qlockfile.pro @@ -0,0 +1,3 @@ +TEMPLATE = subdirs + +SUBDIRS += tst_qlockfile.pro qlockfiletesthelper/qlockfile_test_helper.pro diff --git a/tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper.cpp b/tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper.cpp new file mode 100644 index 0000000000..63f6291034 --- /dev/null +++ b/tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper.cpp @@ -0,0 +1,78 @@ +/**************************************************************************** +** +** Copyright (C) 2013 David Faure +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + if (argc <= 1) + return -1; + + const QString lockName = QString::fromLocal8Bit(argv[1]); + + QString option; + if (argc > 2) + option = QString::fromLocal8Bit(argv[2]); + + if (option == "-crash") { + QLockFile *lockFile = new QLockFile(lockName); + lockFile->lock(); + // leak the lockFile on purpose, so that the lock remains! + return 0; + } else if (option == "-busy") { + QLockFile lockFile(lockName); + lockFile.lock(); + QThread::msleep(500); + return 0; + } else { + QLockFile lockFile(lockName); + if (lockFile.isLocked()) // cannot happen, before calling lock or tryLock + return QLockFile::UnknownError; + + lockFile.tryLock(); + return lockFile.error(); + } +} diff --git a/tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper.pro b/tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper.pro new file mode 100644 index 0000000000..3ac3be9c9b --- /dev/null +++ b/tests/auto/corelib/io/qlockfile/qlockfiletesthelper/qlockfile_test_helper.pro @@ -0,0 +1,7 @@ +TARGET = qlockfile_test_helper +SOURCES += qlockfile_test_helper.cpp + +CONFIG += console +CONFIG -= app_bundle +QT = core +DESTDIR = ./ diff --git a/tests/auto/corelib/io/qlockfile/tst_qlockfile.cpp b/tests/auto/corelib/io/qlockfile/tst_qlockfile.cpp new file mode 100644 index 0000000000..4aed11a2aa --- /dev/null +++ b/tests/auto/corelib/io/qlockfile/tst_qlockfile.cpp @@ -0,0 +1,379 @@ +/**************************************************************************** +** +** Copyright (C) 2013 David Faure +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include +#include +#include +#include + +class tst_QLockFile : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void lockUnlock(); + void lockOutOtherProcess(); + void lockOutOtherThread(); + void waitForLock_data(); + void waitForLock(); + void staleLockFromCrashedProcess_data(); + void staleLockFromCrashedProcess(); + void staleShortLockFromBusyProcess(); + void staleLongLockFromBusyProcess(); + void staleLockRace(); + void noPermissions(); + +public: + QString m_helperApp; + QTemporaryDir dir; +}; + +void tst_QLockFile::initTestCase() +{ +#ifdef QT_NO_PROCESS + QSKIP("This test requires QProcess support"); +#else + // chdir to our testdata path and execute helper apps relative to that. + QString testdata_dir = QFileInfo(QFINDTESTDATA("qlockfiletesthelper")).absolutePath(); + QVERIFY2(QDir::setCurrent(testdata_dir), qPrintable("Could not chdir to " + testdata_dir)); + m_helperApp = "qlockfiletesthelper/qlockfile_test_helper"; +#endif +} + +void tst_QLockFile::lockUnlock() +{ + const QString fileName = dir.path() + "/lock1"; + QVERIFY(!QFile(fileName).exists()); + QLockFile lockFile(fileName); + QVERIFY(lockFile.lock()); + QVERIFY(lockFile.isLocked()); + QCOMPARE(int(lockFile.error()), int(QLockFile::NoError)); + QVERIFY(QFile::exists(fileName)); + + // Recursive locking is not allowed + // (can't test lock() here, it would wait forever) + QVERIFY(!lockFile.tryLock()); + QCOMPARE(int(lockFile.error()), int(QLockFile::LockFailedError)); + qint64 pid; + QString hostname, appname; + QVERIFY(lockFile.getLockInfo(&pid, &hostname, &appname)); + QCOMPARE(pid, QCoreApplication::applicationPid()); + QCOMPARE(appname, qAppName()); + QVERIFY(!lockFile.tryLock(200)); + QCOMPARE(int(lockFile.error()), int(QLockFile::LockFailedError)); + + // Unlock deletes the lock file + lockFile.unlock(); + QCOMPARE(int(lockFile.error()), int(QLockFile::NoError)); + QVERIFY(!lockFile.isLocked()); + QVERIFY(!QFile::exists(fileName)); +} + +void tst_QLockFile::lockOutOtherProcess() +{ + // Lock + const QString fileName = dir.path() + "/lockOtherProcess"; + QLockFile lockFile(fileName); + QVERIFY(lockFile.lock()); + + // Other process can't acquire lock + QProcess proc; + proc.start(m_helperApp, QStringList() << fileName); + QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString())); + QVERIFY(proc.waitForFinished()); + QCOMPARE(proc.exitCode(), int(QLockFile::LockFailedError)); + + // Unlock + lockFile.unlock(); + QVERIFY(!QFile::exists(fileName)); + + // Other process can now acquire lock + int ret = QProcess::execute(m_helperApp, QStringList() << fileName); + QCOMPARE(ret, int(QLockFile::NoError)); + // Lock doesn't survive process though (on clean exit) + QVERIFY(!QFile::exists(fileName)); +} + +static QLockFile::LockError tryLockFromThread(const QString &fileName) +{ + QLockFile lockInThread(fileName); + lockInThread.tryLock(); + return lockInThread.error(); +} + +void tst_QLockFile::lockOutOtherThread() +{ + const QString fileName = dir.path() + "/lockOtherThread"; + QLockFile lockFile(fileName); + QVERIFY(lockFile.lock()); + + // Other thread can't acquire lock + QFuture ret = QtConcurrent::run(tryLockFromThread, fileName); + QCOMPARE(ret.result(), QLockFile::LockFailedError); + + lockFile.unlock(); + + // Now other thread can acquire lock + QFuture ret2 = QtConcurrent::run(tryLockFromThread, fileName); + QCOMPARE(ret2.result(), QLockFile::NoError); +} + +static bool lockFromThread(const QString &fileName, int sleepMs, QSemaphore *semThreadReady, QSemaphore *semMainThreadDone) +{ + QLockFile lockFile(fileName); + if (!lockFile.lock()) { + qWarning() << "Locking failed" << lockFile.error(); + return false; + } + semThreadReady->release(); + QThread::msleep(sleepMs); + semMainThreadDone->acquire(); + lockFile.unlock(); + return true; +} + +void tst_QLockFile::waitForLock_data() +{ + QTest::addColumn("testNumber"); + QTest::addColumn("threadSleepMs"); + QTest::addColumn("releaseEarly"); + QTest::addColumn("tryLockTimeout"); + QTest::addColumn("expectedResult"); + + int tn = 0; // test number + QTest::newRow("wait_forever_succeeds") << ++tn << 500 << true << -1 << true; + QTest::newRow("wait_longer_succeeds") << ++tn << 500 << true << 1000 << true; + QTest::newRow("wait_zero_fails") << ++tn << 500 << false << 0 << false; + QTest::newRow("wait_not_enough_fails") << ++tn << 500 << false << 100 << false; +} + +void tst_QLockFile::waitForLock() +{ + QFETCH(int, testNumber); + QFETCH(int, threadSleepMs); + QFETCH(bool, releaseEarly); + QFETCH(int, tryLockTimeout); + QFETCH(bool, expectedResult); + + const QString fileName = dir.path() + "/waitForLock" + QString::number(testNumber); + QLockFile lockFile(fileName); + QSemaphore semThreadReady, semMainThreadDone; + // Lock file from a thread + QFuture ret = QtConcurrent::run(lockFromThread, fileName, threadSleepMs, &semThreadReady, &semMainThreadDone); + semThreadReady.acquire(); + + if (releaseEarly) // let the thread release the lock after threadSleepMs + semMainThreadDone.release(); + + QCOMPARE(lockFile.tryLock(tryLockTimeout), expectedResult); + if (expectedResult) + QCOMPARE(int(lockFile.error()), int(QLockFile::NoError)); + else + QCOMPARE(int(lockFile.error()), int(QLockFile::LockFailedError)); + + if (!releaseEarly) // only let the thread release the lock now + semMainThreadDone.release(); + + QVERIFY(ret); // waits for the thread to finish +} + +void tst_QLockFile::staleLockFromCrashedProcess_data() +{ + QTest::addColumn("staleLockTime"); + + // Test both use cases for QLockFile, should make no difference here. + QTest::newRow("short") << 30000; + QTest::newRow("long") << 0; +} + +void tst_QLockFile::staleLockFromCrashedProcess() +{ + QFETCH(int, staleLockTime); + const QString fileName = dir.path() + "/staleLockFromCrashedProcess"; + + int ret = QProcess::execute(m_helperApp, QStringList() << fileName << "-crash"); + QCOMPARE(ret, int(QLockFile::NoError)); + QTRY_VERIFY(QFile::exists(fileName)); + + QLockFile secondLock(fileName); + secondLock.setStaleLockTime(staleLockTime); + // tryLock detects and removes the stale lock (since the PID is dead) +#ifdef Q_OS_WIN + // It can take a bit of time on Windows, though. + QVERIFY(secondLock.tryLock(2000)); +#else + QVERIFY(secondLock.tryLock()); +#endif + QCOMPARE(int(secondLock.error()), int(QLockFile::NoError)); +} + +void tst_QLockFile::staleShortLockFromBusyProcess() +{ + const QString fileName = dir.path() + "/staleLockFromBusyProcess"; + + QProcess proc; + proc.start(m_helperApp, QStringList() << fileName << "-busy"); + QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString())); + QTRY_VERIFY(QFile::exists(fileName)); + + QLockFile secondLock(fileName); + QVERIFY(!secondLock.tryLock()); // held by other process + QCOMPARE(int(secondLock.error()), int(QLockFile::LockFailedError)); + qint64 pid; + QString hostname, appname; + QTRY_VERIFY(secondLock.getLockInfo(&pid, &hostname, &appname)); +#ifdef Q_OS_UNIX + QCOMPARE(pid, proc.pid()); +#endif + + secondLock.setStaleLockTime(100); + QTest::qSleep(100); // make the lock stale + // We can't "steal" (delete+recreate) a lock file from a running process + // until the file descriptor is closed. + QVERIFY(!secondLock.tryLock()); + + proc.waitForFinished(); + QVERIFY(secondLock.tryLock()); +} + +void tst_QLockFile::staleLongLockFromBusyProcess() +{ + const QString fileName = dir.path() + "/staleLockFromBusyProcess"; + + QProcess proc; + proc.start(m_helperApp, QStringList() << fileName << "-busy"); + QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString())); + QTRY_VERIFY(QFile::exists(fileName)); + + QLockFile secondLock(fileName); + secondLock.setStaleLockTime(0); + QVERIFY(!secondLock.tryLock(100)); // never stale + QCOMPARE(int(secondLock.error()), int(QLockFile::LockFailedError)); + qint64 pid; + QTRY_VERIFY(secondLock.getLockInfo(&pid, NULL, NULL)); + QVERIFY(pid > 0); + + // As long as the other process is running, we can't remove the lock file + QVERIFY(!secondLock.removeStaleLockFile()); + + proc.waitForFinished(); +} + +static QString tryStaleLockFromThread(const QString &fileName) +{ + QLockFile lockInThread(fileName + ".lock"); + lockInThread.setStaleLockTime(1000); + if (!lockInThread.lock()) + return "Error locking: " + QString::number(lockInThread.error()); + + // The concurrent use of the file below (write, read, delete) is protected by the lock file above. + // (provided that it doesn't become stale due to this operation taking too long) + QFile theFile(fileName); + if (!theFile.open(QIODevice::WriteOnly)) + return "Couldn't open for write"; + theFile.write("Hello world"); + theFile.flush(); + theFile.close(); + QFile reader(fileName); + if (!reader.open(QIODevice::ReadOnly)) + return "Couldn't open for read"; + const QByteArray read = reader.readAll(); + if (read != "Hello world") + return "File didn't have the expected contents:" + read; + reader.remove(); + return QString(); +} + +void tst_QLockFile::staleLockRace() +{ + // Multiple threads notice a stale lock at the same time + // Only one thread should delete it, otherwise a race will ensue + const QString fileName = dir.path() + "/sharedFile"; + const QString lockName = fileName + ".lock"; + int ret = QProcess::execute(m_helperApp, QStringList() << lockName << "-crash"); + QCOMPARE(ret, int(QLockFile::NoError)); + QTRY_VERIFY(QFile::exists(lockName)); + + QThreadPool::globalInstance()->setMaxThreadCount(10); + QFutureSynchronizer synchronizer; + for (int i = 0; i < 8; ++i) + synchronizer.addFuture(QtConcurrent::run(tryStaleLockFromThread, fileName)); + synchronizer.waitForFinished(); + foreach (const QFuture &future, synchronizer.futures()) + QVERIFY2(future.result().isEmpty(), qPrintable(future.result())); +} + +void tst_QLockFile::noPermissions() +{ +#ifdef Q_OS_WIN + // A readonly directory still allows us to create files, on Windows. + QSKIP("No permission testing on Windows"); +#endif + // Restore permissions so that the QTemporaryDir cleanup can happen + class PermissionRestorer + { + QString m_path; + public: + PermissionRestorer(const QString& path) + : m_path(path) + {} + + ~PermissionRestorer() + { + QFile file(m_path); + file.setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)); + } + }; + + const QString fileName = dir.path() + "/staleLock"; + QFile dirAsFile(dir.path()); // I have to use QFile to change a dir's permissions... + QVERIFY2(dirAsFile.setPermissions(QFile::Permissions(0)), qPrintable(dir.path())); // no permissions + PermissionRestorer permissionRestorer(dir.path()); + + QLockFile lockFile(fileName); + QVERIFY(!lockFile.lock()); + QCOMPARE(int(lockFile.error()), int(QLockFile::PermissionError)); +} + +QTEST_MAIN(tst_QLockFile) +#include "tst_qlockfile.moc" diff --git a/tests/auto/corelib/io/qlockfile/tst_qlockfile.pro b/tests/auto/corelib/io/qlockfile/tst_qlockfile.pro new file mode 100644 index 0000000000..2f7009b736 --- /dev/null +++ b/tests/auto/corelib/io/qlockfile/tst_qlockfile.pro @@ -0,0 +1,6 @@ +CONFIG += testcase +CONFIG -= app_bundle +TARGET = tst_qlockfile +SOURCES += tst_qlockfile.cpp + +QT = core testlib concurrent