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 <thiago.macieira@intel.com>
This commit is contained in:
David Faure 2013-02-03 12:00:50 +01:00 committed by The Qt Project
parent 6f8bc4de40
commit 1b582d64eb
15 changed files with 1370 additions and 0 deletions

1
.gitignore vendored
View File

@ -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

View File

@ -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 \

View File

@ -0,0 +1,346 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** 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 <QtCore/qthread.h>
#include <QtCore/qelapsedtimer.h>
#include <QtCore/qdatetime.h>
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

View File

@ -0,0 +1,91 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** 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 <QtCore/qstring.h>
#include <QtCore/qscopedpointer.h>
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<QLockFilePrivate> d_ptr;
private:
Q_DECLARE_PRIVATE(QLockFile)
Q_DISABLE_COPY(QLockFile)
};
QT_END_NAMESPACE
QT_END_HEADER
#endif // QLOCKFILE_H

View File

@ -0,0 +1,104 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** 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 <QtCore/qlockfile.h>
#include <QtCore/qfile.h>
#ifdef Q_OS_WIN
#include <qt_windows.h>
#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 */

View File

@ -0,0 +1,207 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** 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 <sys/file.h> // flock
#include <sys/types.h> // kill
#include <signal.h> // 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

View File

@ -0,0 +1,138 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** 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 <qt_windows.h>
#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

View File

@ -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)
};

View File

@ -62,6 +62,8 @@ protected:
QString templateName;
static QString defaultTemplateName();
friend class QLockFilePrivate;
};
class QTemporaryFileEngine : public QFSFileEngine

View File

@ -14,6 +14,7 @@ SUBDIRS=\
qfilesystemwatcher \
qiodevice \
qipaddress \
qlockfile \
qnodebug \
qprocess \
qprocess-noapplication \

View File

@ -0,0 +1,3 @@
TEMPLATE = subdirs
SUBDIRS += tst_qlockfile.pro qlockfiletesthelper/qlockfile_test_helper.pro

View File

@ -0,0 +1,78 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** 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 <QDebug>
#include <QCoreApplication>
#include <QLockFile>
#include <QThread>
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();
}
}

View File

@ -0,0 +1,7 @@
TARGET = qlockfile_test_helper
SOURCES += qlockfile_test_helper.cpp
CONFIG += console
CONFIG -= app_bundle
QT = core
DESTDIR = ./

View File

@ -0,0 +1,379 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** 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 <QtTest/QtTest>
#include <QtConcurrentRun>
#include <qlockfile.h>
#include <qtemporarydir.h>
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<QLockFile::LockError> ret = QtConcurrent::run<QLockFile::LockError>(tryLockFromThread, fileName);
QCOMPARE(ret.result(), QLockFile::LockFailedError);
lockFile.unlock();
// Now other thread can acquire lock
QFuture<QLockFile::LockError> ret2 = QtConcurrent::run<QLockFile::LockError>(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<int>("testNumber");
QTest::addColumn<int>("threadSleepMs");
QTest::addColumn<bool>("releaseEarly");
QTest::addColumn<int>("tryLockTimeout");
QTest::addColumn<bool>("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<bool> ret = QtConcurrent::run<bool>(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<int>("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<QString> synchronizer;
for (int i = 0; i < 8; ++i)
synchronizer.addFuture(QtConcurrent::run<QString>(tryStaleLockFromThread, fileName));
synchronizer.waitForFinished();
foreach (const QFuture<QString> &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"

View File

@ -0,0 +1,6 @@
CONFIG += testcase
CONFIG -= app_bundle
TARGET = tst_qlockfile
SOURCES += tst_qlockfile.cpp
QT = core testlib concurrent