File indexing completed on 2024-04-21 14:55:38

0001 /*
0002    This file is part of the KDE libraries
0003    Copyright (c) 2004 Waldo Bastian <bastian@kde.org>
0004    Copyright (c) 2011 David Faure <faure@kde.org>
0005 
0006    This library is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU Library General Public
0008    License version 2 as published by the Free Software Foundation.
0009 
0010    This library is distributed in the hope that it will be useful,
0011    but WITHOUT ANY WARRANTY; without even the implied warranty of
0012    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013    Library General Public License for more details.
0014 
0015    You should have received a copy of the GNU Library General Public License
0016    along with this library; see the file COPYING.LIB.  If not, write to
0017    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0018    Boston, MA 02110-1301, USA.
0019 */
0020 
0021 #include "klockfile.h"
0022 
0023 #include <config-klockfile.h>
0024 
0025 #include <sys/types.h>
0026 #if HAVE_SYS_STAT_H
0027 #include <sys/stat.h>
0028 #endif
0029 #if HAVE_SYS_TIME_H
0030 #include <sys/time.h>
0031 #endif
0032 #include <signal.h>
0033 #include <errno.h>
0034 #include <stdlib.h>
0035 #include <unistd.h>
0036 
0037 #include <qplatformdefs.h> // QT_STATBUF, QT_LSTAT, QT_OPEN
0038 #include <QDate>
0039 #include <QFile>
0040 #include <QCoreApplication>
0041 #include <QTextStream>
0042 #include <QTemporaryFile>
0043 
0044 #include "krandom.h"
0045 #include "kfilesystemtype.h"
0046 
0047 #include <unistd.h>
0048 #include <fcntl.h>
0049 
0050 // Related reading:
0051 // http://www.spinnaker.de/linux/nfs-locking.html
0052 // http://en.wikipedia.org/wiki/File_locking
0053 // http://apenwarr.ca/log/?m=201012
0054 
0055 // Related source code:
0056 // * lockfile-create, from the lockfile-progs package, uses the link() trick from lockFileWithLink
0057 // below, so it works over NFS but fails on FAT32 too.
0058 // * the flock program, which uses flock(LOCK_EX), works on local filesystems (including FAT32),
0059 //    but not NFS.
0060 //  Note about flock: don't unlink, it creates a race. http://world.std.com/~swmcd/steven/tech/flock.html
0061 
0062 // fcntl(F_SETLK) is not a good solution.
0063 // It locks other processes but locking out other threads must be done by hand,
0064 // and worse, it unlocks when just reading the file in the same process (!).
0065 // See the apenwarr.ca article above.
0066 
0067 // open(O_EXCL) seems to be the best solution for local files (on all filesystems),
0068 // it only fails over NFS (at least with old NFS servers).
0069 // See http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=144
0070 
0071 // Conclusion: we use O_EXCL by default, and the link() trick over NFS.
0072 
0073 class Q_DECL_HIDDEN KLockFile::Private
0074 {
0075 public:
0076     Private(const QString &componentName)
0077         : staleTime(30), // 30 seconds
0078           isLocked(false),
0079           linkCountSupport(true),
0080           mustCloseFd(false),
0081           m_pid(-1),
0082           m_componentName(componentName)
0083     {
0084     }
0085 
0086     // The main method
0087     KLockFile::LockResult lockFile(QT_STATBUF &st_buf);
0088 
0089     // Two different implementations
0090     KLockFile::LockResult lockFileOExcl(QT_STATBUF &st_buf);
0091     KLockFile::LockResult lockFileWithLink(QT_STATBUF &st_buf);
0092 
0093     KLockFile::LockResult deleteStaleLock();
0094     KLockFile::LockResult deleteStaleLockWithLink();
0095 
0096     void writeIntoLockFile(QFile &file);
0097     void readLockFile();
0098     bool isNfs() const;
0099 
0100     QFile m_file;
0101     QString m_fileName;
0102     int staleTime;
0103     bool isLocked;
0104     bool linkCountSupport;
0105     bool mustCloseFd;
0106     QTime staleTimer;
0107     QT_STATBUF statBuf;
0108     int m_pid;
0109     QString m_hostname;
0110     QString m_componentName; // as set for this instance
0111     QString m_componentNameFromFile; // as read from the lock file
0112 };
0113 
0114 KLockFile::KLockFile(const QString &file, const QString &componentName)
0115     : d(new Private(componentName))
0116 {
0117     d->m_fileName = file;
0118 }
0119 
0120 KLockFile::~KLockFile()
0121 {
0122     unlock();
0123     delete d;
0124 }
0125 
0126 int
0127 KLockFile::staleTime() const
0128 {
0129     return d->staleTime;
0130 }
0131 
0132 void
0133 KLockFile::setStaleTime(int _staleTime)
0134 {
0135     d->staleTime = _staleTime;
0136 }
0137 
0138 static bool operator==(const QT_STATBUF &st_buf1,
0139                        const QT_STATBUF &st_buf2)
0140 {
0141 #define FIELD_EQ(what)       (st_buf1.what == st_buf2.what)
0142     return FIELD_EQ(st_dev) && FIELD_EQ(st_ino) &&
0143            FIELD_EQ(st_uid) && FIELD_EQ(st_gid) && FIELD_EQ(st_nlink);
0144 #undef FIELD_EQ
0145 }
0146 
0147 static bool operator!=(const QT_STATBUF &st_buf1,
0148                        const QT_STATBUF &st_buf2)
0149 {
0150     return !(st_buf1 == st_buf2);
0151 }
0152 
0153 static bool testLinkCountSupport(const QByteArray &fileName)
0154 {
0155     QT_STATBUF st_buf;
0156     int result = -1;
0157     // Check if hardlinks raise the link count at all?
0158     if (!::link(fileName.data(), QByteArray(fileName + ".test").data())) {
0159         result = QT_LSTAT(fileName.data(), &st_buf);
0160         ::unlink(QByteArray(fileName + ".test").data());
0161     }
0162     return (result < 0 || ((result == 0) && (st_buf.st_nlink == 2)));
0163 }
0164 
0165 void KLockFile::Private::writeIntoLockFile(QFile &file)
0166 {
0167     file.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ReadGroup | QFile::ReadOther);
0168 
0169     char hostname[256];
0170     hostname[0] = 0;
0171     gethostname(hostname, 255);
0172     hostname[255] = 0;
0173     m_hostname = QString::fromLocal8Bit(hostname);
0174     if (m_componentName.isEmpty() && QCoreApplication::instance()) { // TODO Qt5: should be fixed by new Q_GLOBAL_STATIC: qcoreappdata() was dangling, in kconfigtest testSyncOnExit.
0175         m_componentName = QCoreApplication::applicationName();
0176     }
0177 
0178     m_pid = getpid();
0179 
0180     file.write(QByteArray::number(m_pid) + '\n');
0181     file.write(m_componentName.toUtf8() + '\n');
0182     file.write(hostname);
0183     file.flush();
0184 }
0185 
0186 void KLockFile::Private::readLockFile()
0187 {
0188     m_pid = -1;
0189     m_hostname.clear();
0190     m_componentNameFromFile.clear();
0191 
0192     QFile file(m_fileName);
0193     if (file.open(QIODevice::ReadOnly)) {
0194         QTextStream ts(&file);
0195         if (!ts.atEnd()) {
0196             m_pid = ts.readLine().toInt();
0197         }
0198         if (!ts.atEnd()) {
0199             m_componentNameFromFile = ts.readLine();
0200         }
0201         if (!ts.atEnd()) {
0202             m_hostname = ts.readLine();
0203         }
0204     }
0205 }
0206 
0207 KLockFile::LockResult KLockFile::Private::lockFileWithLink(QT_STATBUF &st_buf)
0208 {
0209     const QByteArray lockFileName = QFile::encodeName(m_fileName);
0210     int result = QT_LSTAT(lockFileName.data(), &st_buf);
0211     if (result == 0) {
0212         return KLockFile::LockFail;
0213     }
0214 
0215     QTemporaryFile uniqueFile;
0216     uniqueFile.setFileTemplate(m_fileName);
0217     if (!uniqueFile.open()) {
0218         return KLockFile::LockError;
0219     }
0220 
0221     writeIntoLockFile(uniqueFile);
0222 
0223     QByteArray uniqueName = QFile::encodeName(uniqueFile.fileName());
0224 
0225     // Create lock file
0226     result = ::link(uniqueName.data(), lockFileName.data());
0227     if (result != 0) {
0228         return KLockFile::LockError;
0229     }
0230 
0231     if (!linkCountSupport) {
0232         return KLockFile::LockOK;
0233     }
0234 
0235     QT_STATBUF st_buf2;
0236     result = QT_LSTAT(uniqueName.data(), &st_buf2);
0237     if (result != 0) {
0238         return KLockFile::LockError;
0239     }
0240 
0241     result = QT_LSTAT(lockFileName.data(), &st_buf);
0242     if (result != 0) {
0243         return KLockFile::LockError;
0244     }
0245 
0246     if (st_buf != st_buf2 || S_ISLNK(st_buf.st_mode) || S_ISLNK(st_buf2.st_mode)) {
0247         // SMBFS supports hardlinks by copying the file, as a result the above test will always fail
0248         // cifs increases link count artifically but the inodes are still different
0249         if ((st_buf2.st_nlink > 1 ||
0250                 ((st_buf.st_nlink == 1) && (st_buf2.st_nlink == 1))) && (st_buf.st_ino != st_buf2.st_ino)) {
0251             linkCountSupport = testLinkCountSupport(uniqueName);
0252             if (!linkCountSupport) {
0253                 return KLockFile::LockOK;    // Link count support is missing... assume everything is OK.
0254             }
0255         }
0256         return KLockFile::LockFail;
0257     }
0258 
0259     return KLockFile::LockOK;
0260 }
0261 
0262 bool KLockFile::Private::isNfs() const
0263 {
0264     const KFileSystemType::Type fsType = KFileSystemType::fileSystemType(m_fileName);
0265     return fsType == KFileSystemType::Nfs;
0266 }
0267 
0268 KLockFile::LockResult KLockFile::Private::lockFile(QT_STATBUF &st_buf)
0269 {
0270     if (isNfs()) {
0271         return lockFileWithLink(st_buf);
0272     }
0273 
0274     return lockFileOExcl(st_buf);
0275 }
0276 
0277 KLockFile::LockResult KLockFile::Private::lockFileOExcl(QT_STATBUF &st_buf)
0278 {
0279     const QByteArray lockFileName = QFile::encodeName(m_fileName);
0280 
0281     int fd = QT_OPEN(lockFileName.constData(), O_WRONLY | O_CREAT | O_EXCL, 0644);
0282     if (fd < 0) {
0283         if (errno == EEXIST) {
0284             // File already exists
0285             if (QT_LSTAT(lockFileName.constData(), &st_buf) != 0) { // caller wants stat buf details
0286                 // File got deleted meanwhile! Clear struct rather than leaving it unset.
0287                 st_buf.st_dev = 0;
0288                 st_buf.st_ino = 0;
0289                 st_buf.st_uid = 0;
0290                 st_buf.st_gid = 0;
0291                 st_buf.st_nlink = 0;
0292             }
0293             return LockFail;
0294         } else {
0295             return LockError;
0296         }
0297     }
0298     // We hold the lock, continue.
0299     if (!m_file.open(fd, QIODevice::WriteOnly)) {
0300         close(fd);
0301         return LockError;
0302     }
0303     writeIntoLockFile(m_file);
0304 
0305     // stat to get the modification time
0306     const int result = QT_LSTAT(QFile::encodeName(m_fileName).data(), &st_buf);
0307     if (result != 0) {
0308         close(fd);
0309         return KLockFile::LockError;
0310     }
0311     mustCloseFd = true;
0312     return KLockFile::LockOK;
0313 }
0314 
0315 KLockFile::LockResult KLockFile::Private::deleteStaleLock()
0316 {
0317     if (isNfs()) {
0318         return deleteStaleLockWithLink();
0319     }
0320 
0321     // I see no way to prevent the race condition here, where we could
0322     // delete a new lock file that another process just got after we
0323     // decided the old one was too stale for us too.
0324     qWarning("WARNING: deleting stale lockfile %s", qPrintable(m_fileName));
0325     QFile::remove(m_fileName);
0326     return LockOK;
0327 }
0328 
0329 KLockFile::LockResult KLockFile::Private::deleteStaleLockWithLink()
0330 {
0331     // This is dangerous, we could be deleting a new lock instead of
0332     // the old stale one, let's be very careful
0333 
0334     // Create temp file
0335     QTemporaryFile *ktmpFile = new QTemporaryFile;
0336     ktmpFile->setFileTemplate(m_fileName);
0337     if (!ktmpFile->open()) {
0338         delete ktmpFile;
0339         return KLockFile::LockError;
0340     }
0341 
0342     const QByteArray lckFile = QFile::encodeName(m_fileName);
0343     const QByteArray tmpFile = QFile::encodeName(ktmpFile->fileName());
0344     delete ktmpFile;
0345 
0346     // link to lock file
0347     if (::link(lckFile.data(), tmpFile.data()) != 0) {
0348         return KLockFile::LockFail;    // Try again later
0349     }
0350 
0351     // check if link count increased with exactly one
0352     // and if the lock file still matches
0353     QT_STATBUF st_buf1;
0354     QT_STATBUF st_buf2;
0355     memcpy(&st_buf1, &statBuf, sizeof(QT_STATBUF));
0356     st_buf1.st_nlink++;
0357     if ((QT_LSTAT(tmpFile.data(), &st_buf2) == 0) && st_buf1 == st_buf2) {
0358         if ((QT_LSTAT(lckFile.data(), &st_buf2) == 0) && st_buf1 == st_buf2) {
0359             // - - if yes, delete lock file, delete temp file, retry lock
0360             qWarning("WARNING: deleting stale lockfile %s", lckFile.data());
0361             ::unlink(lckFile.data());
0362             ::unlink(tmpFile.data());
0363             return KLockFile::LockOK;
0364         }
0365     }
0366 
0367     // SMBFS supports hardlinks by copying the file, as a result the above test will always fail
0368     if (linkCountSupport) {
0369         linkCountSupport = testLinkCountSupport(tmpFile);
0370     }
0371 
0372     if (!linkCountSupport) {
0373         // Without support for link counts we will have a little race condition
0374         qWarning("WARNING: deleting stale lockfile %s", lckFile.data());
0375         ::unlink(tmpFile.data());
0376         if (::unlink(lckFile.data()) < 0) {
0377             qWarning("WARNING: Problem deleting stale lockfile %s: %s", lckFile.data(),
0378                      strerror(errno));
0379             return KLockFile::LockFail;
0380         }
0381         return KLockFile::LockOK;
0382     }
0383 
0384     // Failed to delete stale lock file
0385     qWarning("WARNING: Problem deleting stale lockfile %s", lckFile.data());
0386     ::unlink(tmpFile.data());
0387     return KLockFile::LockFail;
0388 }
0389 
0390 KLockFile::LockResult KLockFile::lock(LockFlags options)
0391 {
0392     if (d->isLocked) {
0393         return KLockFile::LockOK;
0394     }
0395 
0396     KLockFile::LockResult result;
0397     int hardErrors = 5;
0398     int n = 5;
0399     while (true) {
0400         QT_STATBUF st_buf;
0401         // Try to create the lock file
0402         result = d->lockFile(st_buf);
0403 
0404         if (result == KLockFile::LockOK) {
0405             d->staleTimer = QTime();
0406             break;
0407         } else if (result == KLockFile::LockError) {
0408             d->staleTimer = QTime();
0409             if (--hardErrors == 0) {
0410                 break;
0411             }
0412         } else { // KLockFile::Fail -- there is already such a file present (e.g. left by a crashed app)
0413             if (!d->staleTimer.isNull() && d->statBuf != st_buf) {
0414                 d->staleTimer = QTime();
0415             }
0416 
0417             if (d->staleTimer.isNull()) {
0418                 memcpy(&(d->statBuf), &st_buf, sizeof(QT_STATBUF));
0419                 d->staleTimer.start();
0420 
0421                 d->readLockFile();
0422             }
0423 
0424             bool isStale = false;
0425             if ((d->m_pid > 0) && !d->m_hostname.isEmpty()) {
0426                 // Check if hostname is us
0427                 char hostname[256];
0428                 hostname[0] = 0;
0429                 gethostname(hostname, 255);
0430                 hostname[255] = 0;
0431 
0432                 if (d->m_hostname == QString::fromLocal8Bit(hostname)) {
0433                     // Check if pid still exists
0434                     int res = ::kill(d->m_pid, 0);
0435                     if ((res == -1) && (errno == ESRCH)) {
0436                         isStale = true;    // pid does not exist
0437                     }
0438                 }
0439             }
0440             if (d->staleTimer.elapsed() > (d->staleTime * 1000)) {
0441                 isStale = true;
0442             }
0443 
0444             if (isStale) {
0445                 if ((options & ForceFlag) == 0) {
0446                     return KLockFile::LockStale;
0447                 }
0448 
0449                 result = d->deleteStaleLock();
0450 
0451                 if (result == KLockFile::LockOK) {
0452                     // Lock deletion successful
0453                     d->staleTimer = QTime();
0454                     continue; // Now try to get the new lock
0455                 } else if (result != KLockFile::LockFail) {
0456                     return result;
0457                 }
0458             }
0459         }
0460 
0461         if (options & NoBlockFlag) {
0462             break;
0463         }
0464 
0465         struct timeval tv;
0466         tv.tv_sec = 0;
0467         tv.tv_usec = n * ((KRandom::random() % 200) + 100);
0468         if (n < 2000) {
0469             n = n * 2;
0470         }
0471 
0472         select(0, nullptr, nullptr, nullptr, &tv);
0473     }
0474     if (result == LockOK) {
0475         d->isLocked = true;
0476     }
0477     return result;
0478 }
0479 
0480 bool KLockFile::isLocked() const
0481 {
0482     return d->isLocked;
0483 }
0484 
0485 void KLockFile::unlock()
0486 {
0487     if (d->isLocked) {
0488         ::unlink(QFile::encodeName(d->m_fileName).data());
0489         if (d->mustCloseFd) {
0490             close(d->m_file.handle());
0491             d->mustCloseFd = false;
0492         }
0493         d->m_file.close();
0494         d->m_pid = -1;
0495         d->isLocked = false;
0496     }
0497 }
0498 
0499 bool KLockFile::getLockInfo(int &pid, QString &hostname, QString &appname)
0500 {
0501     if (d->m_pid == -1) {
0502         return false;
0503     }
0504     pid = d->m_pid;
0505     hostname = d->m_hostname;
0506     appname = d->m_componentNameFromFile;
0507     return true;
0508 }