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:
parent
6f8bc4de40
commit
1b582d64eb
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||
|
@ -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 \
|
||||
|
||||
|
346
src/corelib/io/qlockfile.cpp
Normal file
346
src/corelib/io/qlockfile.cpp
Normal 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
|
91
src/corelib/io/qlockfile.h
Normal file
91
src/corelib/io/qlockfile.h
Normal 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
|
104
src/corelib/io/qlockfile_p.h
Normal file
104
src/corelib/io/qlockfile_p.h
Normal 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 */
|
207
src/corelib/io/qlockfile_unix.cpp
Normal file
207
src/corelib/io/qlockfile_unix.cpp
Normal 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
|
138
src/corelib/io/qlockfile_win.cpp
Normal file
138
src/corelib/io/qlockfile_win.cpp
Normal 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
|
@ -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)
|
||||
};
|
||||
|
||||
|
@ -62,6 +62,8 @@ protected:
|
||||
QString templateName;
|
||||
|
||||
static QString defaultTemplateName();
|
||||
|
||||
friend class QLockFilePrivate;
|
||||
};
|
||||
|
||||
class QTemporaryFileEngine : public QFSFileEngine
|
||||
|
@ -14,6 +14,7 @@ SUBDIRS=\
|
||||
qfilesystemwatcher \
|
||||
qiodevice \
|
||||
qipaddress \
|
||||
qlockfile \
|
||||
qnodebug \
|
||||
qprocess \
|
||||
qprocess-noapplication \
|
||||
|
3
tests/auto/corelib/io/qlockfile/qlockfile.pro
Normal file
3
tests/auto/corelib/io/qlockfile/qlockfile.pro
Normal file
@ -0,0 +1,3 @@
|
||||
TEMPLATE = subdirs
|
||||
|
||||
SUBDIRS += tst_qlockfile.pro qlockfiletesthelper/qlockfile_test_helper.pro
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
TARGET = qlockfile_test_helper
|
||||
SOURCES += qlockfile_test_helper.cpp
|
||||
|
||||
CONFIG += console
|
||||
CONFIG -= app_bundle
|
||||
QT = core
|
||||
DESTDIR = ./
|
379
tests/auto/corelib/io/qlockfile/tst_qlockfile.cpp
Normal file
379
tests/auto/corelib/io/qlockfile/tst_qlockfile.cpp
Normal 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"
|
6
tests/auto/corelib/io/qlockfile/tst_qlockfile.pro
Normal file
6
tests/auto/corelib/io/qlockfile/tst_qlockfile.pro
Normal file
@ -0,0 +1,6 @@
|
||||
CONFIG += testcase
|
||||
CONFIG -= app_bundle
|
||||
TARGET = tst_qlockfile
|
||||
SOURCES += tst_qlockfile.cpp
|
||||
|
||||
QT = core testlib concurrent
|
Loading…
Reference in New Issue
Block a user