Blob Blame History Raw
commit 63b8ad8b7c6cc9845f11e5fbacf1a8cc9c977859
Author: David Faure <faure@kde.org>
Date:   Sat Aug 6 14:53:36 2011 +0200

    Implement locking on non-NFS systems using O_EXCL
    
    Fixes locking on VFAT and CIFS etc.
    
    The unit-test only tests local files automatically, but explains
    how to set up test environments to test it on a FAT filesystem and on a NFS mount.
    
    BUG: 203554
    (cherry picked from commit 10ca4ddc8cca49103c2d605a64468788605f3010)

diff --git a/kdecore/io/klockfile_unix.cpp b/kdecore/io/klockfile_unix.cpp
index cd909cf..fa2eda4 100644
--- a/kdecore/io/klockfile_unix.cpp
+++ b/kdecore/io/klockfile_unix.cpp
@@ -1,6 +1,7 @@
 /*
    This file is part of the KDE libraries
    Copyright (c) 2004 Waldo Bastian <bastian@kde.org>
+   Copyright (c) 2011 David Faure <faure@kde.org>
 
    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
@@ -35,48 +36,85 @@
 
 #include <QtCore/QDate>
 #include <QtCore/QFile>
-#include <QtCore/QTextIStream>
+#include <QTextStream>
 
 #include "krandom.h"
 #include "kglobal.h"
 #include "kcomponentdata.h"
 #include "ktemporaryfile.h"
 #include "kde_file.h"
+#include "kfilesystemtype_p.h"
 
-// TODO: http://www.spinnaker.de/linux/nfs-locking.html
+#include <unistd.h>
+#include <fcntl.h>
+
+// Related reading:
+// http://www.spinnaker.de/linux/nfs-locking.html
+// http://en.wikipedia.org/wiki/File_locking
+// http://apenwarr.ca/log/?m=201012
+
+// Related source code:
+// * lockfile-create, from the lockfile-progs package, uses the link() trick from lockFileWithLink
+// below, so it works over NFS but fails on FAT32 too.
+// * the flock program, which uses flock(LOCK_EX), works on local filesystems (including FAT32),
+//    but not NFS.
+//  Note about flock: don't unlink, it creates a race. http://world.std.com/~swmcd/steven/tech/flock.html
+
+// fcntl(F_SETLK) is not a good solution.
+// It locks other processes but locking out other threads must be done by hand,
+// and worse, it unlocks when just reading the file in the same process (!).
+// See the apenwarr.ca article above.
+
+// open(O_EXCL) seems to be the best solution for local files (on all filesystems),
+// it only fails over NFS (at least with old NFS servers).
+// See http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=144
+
+// Conclusion: we use O_EXCL by default, and the link() trick over NFS.
 
 class KLockFile::Private
 {
 public:
     Private(const KComponentData &c)
-        : componentData(c)
+        : staleTime(30), // 30 seconds
+          isLocked(false),
+          linkCountSupport(true),
+          m_pid(-1),
+          m_componentData(c)
     {
     }
 
-    QString file;
+    // The main method
+    KLockFile::LockResult lockFile(KDE_struct_stat &st_buf);
+
+    // Two different implementations
+    KLockFile::LockResult lockFileOExcl(KDE_struct_stat &st_buf);
+    KLockFile::LockResult lockFileWithLink(KDE_struct_stat &st_buf);
+
+    KLockFile::LockResult deleteStaleLock();
+    KLockFile::LockResult deleteStaleLockWithLink();
+
+    void writeIntoLockFile(QFile& file, const KComponentData& componentData);
+    void readLockFile();
+    bool isNfs() const;
+
+    QFile m_file;
+    QString m_fileName;
     int staleTime;
     bool isLocked;
-    bool recoverLock;
     bool linkCountSupport;
     QTime staleTimer;
     KDE_struct_stat statBuf;
-    int pid;
-    QString hostname;
-    QString instance;
-    QString lockRecoverFile;
-    KComponentData componentData;
+    int m_pid;
+    QString m_hostname;
+    QString m_componentName;
+    KComponentData m_componentData;
 };
 
 
-// 30 seconds
 KLockFile::KLockFile(const QString &file, const KComponentData &componentData)
     : d(new Private(componentData))
 {
-  d->file = file;
-  d->staleTime = 30;
-  d->isLocked = false;
-  d->recoverLock = false;
-  d->linkCountSupport = true;
+  d->m_fileName = file;
 }
 
 KLockFile::~KLockFile()
@@ -125,31 +163,59 @@ static bool testLinkCountSupport(const QByteArray &fileName)
    return (result < 0 || ((result == 0) && (st_buf.st_nlink == 2)));
 }
 
-static KLockFile::LockResult lockFile(const QString &lockFile, KDE_struct_stat &st_buf,
-        bool &linkCountSupport, const KComponentData &componentData)
+void KLockFile::Private::writeIntoLockFile(QFile& file, const KComponentData& componentData)
 {
-  QByteArray lockFileName = QFile::encodeName( lockFile );
-  int result = KDE_lstat( lockFileName, &st_buf );
-  if (result == 0)
-     return KLockFile::LockFail;
-
-  KTemporaryFile uniqueFile(componentData);
-  uniqueFile.setFileTemplate(lockFile);
-  if (!uniqueFile.open())
-     return KLockFile::LockError;
-  uniqueFile.setPermissions(QFile::ReadUser|QFile::WriteUser|QFile::ReadGroup|QFile::ReadOther);
+  file.setPermissions(QFile::ReadUser|QFile::WriteUser|QFile::ReadGroup|QFile::ReadOther);
 
   char hostname[256];
   hostname[0] = 0;
   gethostname(hostname, 255);
   hostname[255] = 0;
-  QString componentName = componentData.componentName();
+  m_hostname = QString::fromLocal8Bit(hostname);
+  m_componentName = componentData.componentName();
+
+  QTextStream stream(&file);
+  m_pid = getpid();
 
-  QTextStream stream(&uniqueFile);
-  stream << QString::number(getpid()) << endl
-      << componentName << endl
-      << hostname << endl;
+  stream << QString::number(m_pid) << endl
+      << m_componentName << endl
+      << m_hostname << endl;
   stream.flush();
+}
+
+void KLockFile::Private::readLockFile()
+{
+    m_pid = -1;
+    m_hostname.clear();
+    m_componentName.clear();
+
+    QFile file(m_fileName);
+    if (file.open(QIODevice::ReadOnly))
+    {
+        QTextStream ts(&file);
+        if (!ts.atEnd())
+            m_pid = ts.readLine().toInt();
+        if (!ts.atEnd())
+            m_componentName = ts.readLine();
+        if (!ts.atEnd())
+            m_hostname = ts.readLine();
+    }
+}
+
+KLockFile::LockResult KLockFile::Private::lockFileWithLink(KDE_struct_stat &st_buf)
+{
+  const QByteArray lockFileName = QFile::encodeName( m_fileName );
+  int result = KDE_lstat( lockFileName, &st_buf );
+  if (result == 0) {
+     return KLockFile::LockFail;
+  }
+
+  KTemporaryFile uniqueFile(m_componentData);
+  uniqueFile.setFileTemplate(m_fileName);
+  if (!uniqueFile.open())
+     return KLockFile::LockError;
+
+  writeIntoLockFile(uniqueFile, m_componentData);
 
   QByteArray uniqueName = QFile::encodeName( uniqueFile.fileName() );
 
@@ -187,20 +253,74 @@ static KLockFile::LockResult lockFile(const QString &lockFile, KDE_struct_stat &
   return KLockFile::LockOK;
 }
 
-static KLockFile::LockResult deleteStaleLock(const QString &lockFile, KDE_struct_stat &st_buf, bool &linkCountSupport, const KComponentData &componentData)
+bool KLockFile::Private::isNfs() const
+{
+    const KFileSystemType::Type fsType = KFileSystemType::fileSystemType(m_fileName);
+    return fsType == KFileSystemType::Nfs;
+}
+
+KLockFile::LockResult KLockFile::Private::lockFile(KDE_struct_stat &st_buf)
+{
+    if (isNfs()) {
+        return lockFileWithLink(st_buf);
+    }
+
+    return lockFileOExcl(st_buf);
+}
+
+KLockFile::LockResult KLockFile::Private::lockFileOExcl(KDE_struct_stat &st_buf)
 {
-   // This is dangerous, we could be deleting a new lock instead of
-   // the old stale one, let's be very careful
+    const QByteArray lockFileName = QFile::encodeName( m_fileName );
+
+    int fd = KDE_open(lockFileName.constData(), O_WRONLY | O_CREAT | O_EXCL, 0644);
+    if (fd < 0) {
+        if (errno == EEXIST) {
+            // File already exists
+            return LockFail;
+        } else {
+            return LockError;
+        }
+    }
+    // We hold the lock, continue.
+    if (!m_file.open(fd, QIODevice::WriteOnly)) {
+        return LockError;
+    }
+    writeIntoLockFile(m_file, m_componentData);
+    const int result = KDE_lstat(QFile::encodeName(m_fileName), &st_buf);
+    if (result != 0)
+        return KLockFile::LockError;
+    return KLockFile::LockOK;
+}
 
-   // Create temp file
-   KTemporaryFile *ktmpFile = new KTemporaryFile(componentData);
-   ktmpFile->setFileTemplate(lockFile);
-   if (!ktmpFile->open())
-      return KLockFile::LockError;
+KLockFile::LockResult KLockFile::Private::deleteStaleLock()
+{
+    if (isNfs())
+        return deleteStaleLockWithLink();
+
+    // I see no way to prevent the race condition here, where we could
+    // delete a new lock file that another process just got after we
+    // decided the old one was too stale for us too.
+    qWarning("WARNING: deleting stale lockfile %s", qPrintable(m_fileName));
+    QFile::remove(m_fileName);
+    return LockOK;
+}
 
-   QByteArray lckFile = QFile::encodeName(lockFile);
-   QByteArray tmpFile = QFile::encodeName(ktmpFile->fileName());
-   delete ktmpFile;
+KLockFile::LockResult KLockFile::Private::deleteStaleLockWithLink()
+{
+    // This is dangerous, we could be deleting a new lock instead of
+    // the old stale one, let's be very careful
+
+    // Create temp file
+    KTemporaryFile *ktmpFile = new KTemporaryFile(m_componentData);
+    ktmpFile->setFileTemplate(m_fileName);
+    if (!ktmpFile->open()) {
+        delete ktmpFile;
+        return KLockFile::LockError;
+    }
+
+    const QByteArray lckFile = QFile::encodeName(m_fileName);
+    const QByteArray tmpFile = QFile::encodeName(ktmpFile->fileName());
+    delete ktmpFile;
 
    // link to lock file
    if (::link(lckFile, tmpFile) != 0)
@@ -210,7 +330,7 @@ static KLockFile::LockResult deleteStaleLock(const QString &lockFile, KDE_struct
    // and if the lock file still matches
    KDE_struct_stat st_buf1;
    KDE_struct_stat st_buf2;
-   memcpy(&st_buf1, &st_buf, sizeof(KDE_struct_stat));
+   memcpy(&st_buf1, &statBuf, sizeof(KDE_struct_stat));
    st_buf1.st_nlink++;
    if ((KDE_lstat(tmpFile, &st_buf2) == 0) && st_buf1 == st_buf2)
    {
@@ -260,8 +380,10 @@ KLockFile::LockResult KLockFile::lock(LockFlags options)
   int n = 5;
   while(true)
   {
-     KDE_struct_stat st_buf;
-     result = lockFile(d->file, st_buf, d->linkCountSupport, d->componentData);
+        KDE_struct_stat st_buf;
+        // Try to create the lock file
+        result = d->lockFile(st_buf);
+
      if (result == KLockFile::LockOK)
      {
         d->staleTimer = QTime();
@@ -285,25 +407,11 @@ KLockFile::LockResult KLockFile::lock(LockFlags options)
            memcpy(&(d->statBuf), &st_buf, sizeof(KDE_struct_stat));
            d->staleTimer.start();
 
-           d->pid = -1;
-           d->hostname.clear();
-           d->instance.clear();
-
-           QFile file(d->file);
-           if (file.open(QIODevice::ReadOnly))
-           {
-              QTextStream ts(&file);
-              if (!ts.atEnd())
-                 d->pid = ts.readLine().toInt();
-              if (!ts.atEnd())
-                 d->instance = ts.readLine();
-              if (!ts.atEnd())
-                 d->hostname = ts.readLine();
-           }
+           d->readLockFile();
         }
 
         bool isStale = false;
-        if ((d->pid > 0) && !d->hostname.isEmpty())
+        if ((d->m_pid > 0) && !d->m_hostname.isEmpty())
         {
            // Check if hostname is us
            char hostname[256];
@@ -311,12 +419,12 @@ KLockFile::LockResult KLockFile::lock(LockFlags options)
            gethostname(hostname, 255);
            hostname[255] = 0;
 
-           if (d->hostname == QLatin1String(hostname))
+           if (d->m_hostname == QLatin1String(hostname))
            {
               // Check if pid still exists
-              int res = ::kill(d->pid, 0);
+              int res = ::kill(d->m_pid, 0);
               if ((res == -1) && (errno == ESRCH))
-                 isStale = true;
+                  isStale = true; // pid does not exist
            }
         }
         if (d->staleTimer.elapsed() > (d->staleTime*1000))
@@ -327,7 +435,7 @@ KLockFile::LockResult KLockFile::lock(LockFlags options)
            if ((options & ForceFlag) == 0)
               return KLockFile::LockStale;
 
-           result = deleteStaleLock(d->file, d->statBuf, d->linkCountSupport, d->componentData);
+           result = d->deleteStaleLock();
 
            if (result == KLockFile::LockOK)
            {
@@ -367,17 +475,19 @@ void KLockFile::unlock()
 {
   if (d->isLocked)
   {
-     ::unlink(QFile::encodeName(d->file));
+     ::unlink(QFile::encodeName(d->m_fileName));
+     d->m_file.close();
+     d->m_pid = -1;
      d->isLocked = false;
   }
 }
 
 bool KLockFile::getLockInfo(int &pid, QString &hostname, QString &appname)
 {
-  if (d->pid == -1)
+  if (d->m_pid == -1)
      return false;
-  pid = d->pid;
-  hostname = d->hostname;
-  appname = d->instance;
+  pid = d->m_pid;
+  hostname = d->m_hostname;
+  appname = d->m_componentName;
   return true;
 }
diff --git a/kdecore/tests/CMakeLists.txt b/kdecore/tests/CMakeLists.txt
index 7c3db77..9173746 100644
--- a/kdecore/tests/CMakeLists.txt
+++ b/kdecore/tests/CMakeLists.txt
@@ -97,6 +97,7 @@ KDECORE_EXECUTABLE_TESTS(
  dbuscalltest
  kmdcodectest
  startserviceby
+ klockfile_testlock # helper for klockfiletest
 )
 
 ########### klocaletest ###############
diff --git a/kdecore/tests/klockfiletest.cpp b/kdecore/tests/klockfiletest.cpp
index 32cdec9..f2c6f91 100644
--- a/kdecore/tests/klockfiletest.cpp
+++ b/kdecore/tests/klockfiletest.cpp
@@ -19,8 +19,12 @@
 #include "klockfiletest.h"
 #include "klockfiletest.moc"
 
+#include <kdebug.h>
 #include <unistd.h>
 
+// TODO test locking from two different threads
+//#include <qtconcurrentrun.h>
+
 namespace QTest {
 template<>
 char* toString(const KLockFile::LockResult& result)
@@ -32,6 +36,26 @@ char* toString(const KLockFile::LockResult& result)
 }
 }
 
+// Here's how to test file locking on a FAT filesystem, on linux:
+// cd /tmp
+// dd if=/dev/zero of=fatfile count=180000
+// mkfs.vfat -F32 fatfile
+// mkdir -p fatsystem
+// sudo mount -o loop -o uid=$UID fatfile fatsystem
+// 
+// static const char *const lockName = "/tmp/fatsystem/klockfiletest.lock";
+//
+// =====================================================================
+// 
+// And here's how to test file locking over NFS, on linux:
+// In /etc/exports:    /tmp/nfs localhost(rw,sync,no_subtree_check)
+// sudo mount -t nfs localhost:/tmp/nfs /tmp/nfs-mount
+// 
+// static const char *const lockName = "/tmp/nfs-mount/klockfiletest.lock";
+//
+// =====================================================================
+// 
+
 static const char *const lockName = "klockfiletest.lock";
 
 void
@@ -41,19 +65,28 @@ Test_KLockFile::initTestCase()
 	lockFile = new KLockFile(QLatin1String(lockName));
 }
 
+static KLockFile::LockResult testLockFromProcess(const QString& lockName)
+{
+    const int ret = QProcess::execute("./klockfile_testlock", QStringList() << lockName);
+    return KLockFile::LockResult(ret);
+}
+
 void
 Test_KLockFile::testLock()
 {
 	QVERIFY(!lockFile->isLocked());
 	QCOMPARE(lockFile->lock(), KLockFile::LockOK);
 	QVERIFY(lockFile->isLocked());
-#ifdef Q_WS_WIN
+
+        // Try to lock it again, should fail
 	KLockFile *lockFile2 = new KLockFile(QLatin1String(lockName));
 	QVERIFY(!lockFile2->isLocked());
-	QCOMPARE(lockFile2->lock(), KLockFile::LockFail);
+	QCOMPARE(lockFile2->lock(KLockFile::NoBlockFlag), KLockFile::LockFail);
 	QVERIFY(!lockFile2->isLocked());
 	delete lockFile2;
-#endif    
+
+        // Also try from a different process.
+        QCOMPARE(testLockFromProcess(QLatin1String(lockName)), KLockFile::LockFail);
 }
 
 void
@@ -102,7 +135,7 @@ Test_KLockFile::testStaleNoBlockFlag()
 #else
     char hostname[256];
     ::gethostname(hostname, sizeof(hostname));
-    
+
     QFile f(lockName);
     f.open(QIODevice::WriteOnly);
     QTextStream stream(&f);
@@ -113,7 +146,8 @@ Test_KLockFile::testStaleNoBlockFlag()
     lockFile = new KLockFile(QLatin1String(lockName));
     QVERIFY(!lockFile->isLocked());
     QCOMPARE(lockFile->lock(KLockFile::NoBlockFlag), KLockFile::LockStale);
-    QTest::ignoreMessage(QtWarningMsg, "WARNING: deleting stale lockfile klockfiletest.lock");
+    QByteArray expectedMsg = QByteArray("WARNING: deleting stale lockfile ") + lockName;
+    QTest::ignoreMessage(QtWarningMsg, expectedMsg);
     QCOMPARE(lockFile->lock(KLockFile::NoBlockFlag|KLockFile::ForceFlag), KLockFile::LockOK);
 
     QVERIFY(lockFile->isLocked());