take process name into account for QLockFile's pid clash resolution

To cover the situation that the process ID got reused, the current
process name is compared to the name of the process that corresponds
to the process ID from the lock file.
If the process names differ, the lock file is considered stale.

[ChangeLog][QtCore][QLockFile] Detection of stale lock files got more
robust and takes the name of the process that belongs to the stored
PID into account.

Task-number: QTBUG-45497
Change-Id: Ic3c0d7e066435451203e77b9b9ce2d70bfb9c570
Reviewed-by: Friedemann Kleint <Friedemann.Kleint@theqtcompany.com>
Reviewed-by: David Faure <david.faure@kdab.com>
This commit is contained in:
Joerg Bornemann 2015-04-29 14:36:24 +02:00
parent a0e5210d8b
commit 80c8d324b3
5 changed files with 150 additions and 4 deletions

View File

@ -1,6 +1,7 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
@ -66,9 +67,12 @@ QT_BEGIN_NAMESPACE
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).
tries to detect such a "stale" lock file, based on the process ID written into the file.
To cover the situation that the process ID got reused meanwhile, the current process name is
compared to the name of the process that corresponds to the process ID from the lock file.
If the process names differ, the lock file is considered stale.
Additionally, the last modification time of the lock file (30s by default, for the use case of a
short-lived operation) is taken into account.
If the lock file is found to be stale, it will be deleted.
For the use case of protecting a resource over a long time, you should therefore call
@ -122,7 +126,7 @@ QLockFile::~QLockFile()
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
meanwhile, so one way to detect a stale lock file is by the fact that
it has been around for a long time.
\sa staleLockTime()

View File

@ -75,6 +75,7 @@ public:
// Returns \c 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;
static QString processNameByPid(qint64 pid);
#ifdef Q_OS_UNIX
static int checkFcntlWorksAfterFlock();

View File

@ -1,6 +1,7 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
@ -48,6 +49,15 @@
#include <signal.h> // kill
#include <unistd.h> // gethostname
#if defined(Q_OS_OSX)
# include <libproc.h>
#elif defined(Q_OS_LINUX)
# include <unistd.h>
# include <cstdio>
#elif defined(Q_OS_BSD4) && !defined(Q_OS_IOS)
# include <sys/user.h>
#endif
QT_BEGIN_NAMESPACE
static QByteArray localHostName() // from QHostInfo::localHostName(), modified to return a QByteArray
@ -189,12 +199,50 @@ bool QLockFilePrivate::isApparentlyStale() const
if (hostname.isEmpty() || hostname == QString::fromLocal8Bit(localHostName())) {
if (::kill(pid, 0) == -1 && errno == ESRCH)
return true; // PID doesn't exist anymore
const QString processName = processNameByPid(pid);
if (!processName.isEmpty()) {
QFileInfo fi(appname);
if (fi.isSymLink())
fi.setFile(fi.symLinkTarget());
if (processName != fi.fileName())
return true; // PID got reused by a different application.
}
}
}
const qint64 age = QFileInfo(fileName).lastModified().msecsTo(QDateTime::currentDateTime());
return staleLockTime > 0 && age > staleLockTime;
}
QString QLockFilePrivate::processNameByPid(qint64 pid)
{
#if defined(Q_OS_OSX)
char name[1024];
proc_name(pid, name, sizeof(name) / sizeof(char));
return QString::fromUtf8(name);
#elif defined(Q_OS_LINUX)
if (!QFile::exists(QStringLiteral("/proc/version")))
return QString();
char exePath[64];
char buf[PATH_MAX];
memset(buf, 0, sizeof(buf));
sprintf(exePath, "/proc/%lld/exe", pid);
if (readlink(exePath, buf, sizeof(buf)) < 0) {
// The pid is gone. Return some invalid process name to fail the test.
return QStringLiteral("/ERROR/");
}
return QFileInfo(QString::fromUtf8(buf)).fileName();
#elif defined(Q_OS_BSD4) && !defined(Q_OS_IOS)
kinfo_proc *proc = kinfo_getproc(pid);
if (!proc)
return QString();
QString name = QString::fromUtf8(proc->ki_comm);
free(proc);
return name;
#else
return QString();
#endif
}
void QLockFile::unlock()
{
Q_D(QLockFile);

View File

@ -1,6 +1,7 @@
/****************************************************************************
**
** Copyright (C) 2013 David Faure <faure+bluesystems@kde.org>
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
@ -140,6 +141,9 @@ bool QLockFilePrivate::isApparentlyStale() const
::CloseHandle(procHandle);
if (dwR == WAIT_TIMEOUT)
return true;
const QString processName = processNameByPid(pid);
if (!processName.isEmpty() && processName != appname)
return true; // PID got reused by a different application.
}
}
#else // !Q_OS_WINRT
@ -151,6 +155,47 @@ bool QLockFilePrivate::isApparentlyStale() const
return staleLockTime > 0 && age > staleLockTime;
}
QString QLockFilePrivate::processNameByPid(qint64 pid)
{
#if !defined(Q_OS_WINRT) && !defined(Q_OS_WINCE)
typedef DWORD (WINAPI *GetModuleFileNameExFunc)(HANDLE, HMODULE, LPTSTR, DWORD);
HMODULE hPsapi = LoadLibraryA("psapi");
if (!hPsapi)
return QString();
GetModuleFileNameExFunc qGetModuleFileNameEx
= (GetModuleFileNameExFunc)GetProcAddress(hPsapi, "GetModuleFileNameExW");
if (!qGetModuleFileNameEx) {
FreeLibrary(hPsapi);
return QString();
}
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, DWORD(pid));
if (!hProcess) {
FreeLibrary(hPsapi);
return QString();
}
wchar_t buf[MAX_PATH];
const DWORD length = qGetModuleFileNameEx(hProcess, NULL, buf, sizeof(buf) / sizeof(wchar_t));
CloseHandle(hProcess);
FreeLibrary(hPsapi);
if (!length)
return QString();
QString name = QString::fromWCharArray(buf, length);
int i = name.lastIndexOf(QLatin1Char('\\'));
if (i >= 0)
name.remove(0, i + 1);
i = name.lastIndexOf(QLatin1Char('.'));
if (i >= 0)
name.truncate(i);
return name;
#else
Q_UNUSED(pid);
return QString();
#endif
}
void QLockFile::unlock()
{
Q_D(QLockFile);

View File

@ -57,6 +57,7 @@ private slots:
void waitForLock();
void staleLockFromCrashedProcess_data();
void staleLockFromCrashedProcess();
void staleLockFromCrashedProcessReusedPid();
void staleShortLockFromBusyProcess();
void staleLongLockFromBusyProcess();
void staleLockRace();
@ -64,6 +65,9 @@ private slots:
void noPermissionsWindows();
void corruptedLockFile();
private:
static bool overwritePidInLockFile(const QString &filePath, qint64 pid);
public:
QString m_helperApp;
QTemporaryDir dir;
@ -277,6 +281,30 @@ void tst_QLockFile::staleLockFromCrashedProcess()
#endif // !QT_NO_PROCESS
}
void tst_QLockFile::staleLockFromCrashedProcessReusedPid()
{
#if defined(QT_NO_PROCESS)
QSKIP("This test requires QProcess support");
#elif defined(Q_OS_WINRT) || defined(Q_OS_WINCE) || defined(Q_OS_IOS)
QSKIP("We cannot retrieve information about other processes on this platform.");
#else
const QString fileName = dir.path() + "/staleLockFromCrashedProcessReusedPid";
int ret = QProcess::execute(m_helperApp, QStringList() << fileName << "-crash");
QCOMPARE(ret, int(QLockFile::NoError));
QVERIFY(QFile::exists(fileName));
QVERIFY(overwritePidInLockFile(fileName, QCoreApplication::applicationPid()));
QLockFile secondLock(fileName);
qint64 pid = 0;
secondLock.getLockInfo(&pid, 0, 0);
QCOMPARE(pid, QCoreApplication::applicationPid());
secondLock.setStaleLockTime(0);
QVERIFY(secondLock.tryLock());
QCOMPARE(int(secondLock.error()), int(QLockFile::NoError));
#endif // !QT_NO_PROCESS
}
void tst_QLockFile::staleShortLockFromBusyProcess()
{
#ifdef QT_NO_PROCESS
@ -497,5 +525,25 @@ void tst_QLockFile::corruptedLockFile()
QCOMPARE(int(secondLock.error()), int(QLockFile::NoError));
}
bool tst_QLockFile::overwritePidInLockFile(const QString &filePath, qint64 pid)
{
QFile f(filePath);
if (!f.open(QFile::ReadWrite)) {
qWarning("Cannot open %s.", qPrintable(filePath));
return false;
}
QByteArray buf = f.readAll();
int i = buf.indexOf('\n');
if (i < 0) {
qWarning("Unexpected lockfile content.");
return false;
}
buf.remove(0, i);
buf.prepend(QByteArray::number(pid));
f.seek(0);
f.resize(buf.size());
return f.write(buf) == buf.size();
}
QTEST_MAIN(tst_QLockFile)
#include "tst_qlockfile.moc"