1 /*
2    This file is part of the KDE libraries
3    Copyright (c) 2004 Waldo Bastian <bastian@kde.org>
4    Copyright (c) 2011 David Faure <faure@kde.org>
5 
6    This library is free software; you can redistribute it and/or
7    modify it under the terms of the GNU Library General Public
8    License version 2 as published by the Free Software Foundation.
9 
10    This library is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    Library General Public License for more details.
14 
15    You should have received a copy of the GNU Library General Public License
16    along with this library; see the file COPYING.LIB.  If not, write to
17    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18    Boston, MA 02110-1301, USA.
19 */
20 
21 #include "klockfile.h"
22 
23 #include <config-klockfile.h>
24 
25 #include <sys/types.h>
26 #if HAVE_SYS_STAT_H
27 #include <sys/stat.h>
28 #endif
29 #if HAVE_SYS_TIME_H
30 #include <sys/time.h>
31 #endif
32 #include <signal.h>
33 #include <errno.h>
34 #include <stdlib.h>
35 #include <unistd.h>
36 
37 #include <qplatformdefs.h> // QT_STATBUF, QT_LSTAT, QT_OPEN
38 #include <QDate>
39 #include <QFile>
40 #include <QCoreApplication>
41 #include <QTextStream>
42 #include <QTemporaryFile>
43 
44 #include "krandom.h"
45 #include "kfilesystemtype.h"
46 
47 #include <unistd.h>
48 #include <fcntl.h>
49 
50 // Related reading:
51 // http://www.spinnaker.de/linux/nfs-locking.html
52 // http://en.wikipedia.org/wiki/File_locking
53 // http://apenwarr.ca/log/?m=201012
54 
55 // Related source code:
56 // * lockfile-create, from the lockfile-progs package, uses the link() trick from lockFileWithLink
57 // below, so it works over NFS but fails on FAT32 too.
58 // * the flock program, which uses flock(LOCK_EX), works on local filesystems (including FAT32),
59 //    but not NFS.
60 //  Note about flock: don't unlink, it creates a race. http://world.std.com/~swmcd/steven/tech/flock.html
61 
62 // fcntl(F_SETLK) is not a good solution.
63 // It locks other processes but locking out other threads must be done by hand,
64 // and worse, it unlocks when just reading the file in the same process (!).
65 // See the apenwarr.ca article above.
66 
67 // open(O_EXCL) seems to be the best solution for local files (on all filesystems),
68 // it only fails over NFS (at least with old NFS servers).
69 // See http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=144
70 
71 // Conclusion: we use O_EXCL by default, and the link() trick over NFS.
72 
73 class Q_DECL_HIDDEN KLockFile::Private
74 {
75 public:
Private(const QString & componentName)76     Private(const QString &componentName)
77         : staleTime(30), // 30 seconds
78           isLocked(false),
79           linkCountSupport(true),
80           mustCloseFd(false),
81           m_pid(-1),
82           m_componentName(componentName)
83     {
84     }
85 
86     // The main method
87     KLockFile::LockResult lockFile(QT_STATBUF &st_buf);
88 
89     // Two different implementations
90     KLockFile::LockResult lockFileOExcl(QT_STATBUF &st_buf);
91     KLockFile::LockResult lockFileWithLink(QT_STATBUF &st_buf);
92 
93     KLockFile::LockResult deleteStaleLock();
94     KLockFile::LockResult deleteStaleLockWithLink();
95 
96     void writeIntoLockFile(QFile &file);
97     void readLockFile();
98     bool isNfs() const;
99 
100     QFile m_file;
101     QString m_fileName;
102     int staleTime;
103     bool isLocked;
104     bool linkCountSupport;
105     bool mustCloseFd;
106     QTime staleTimer;
107     QT_STATBUF statBuf;
108     int m_pid;
109     QString m_hostname;
110     QString m_componentName; // as set for this instance
111     QString m_componentNameFromFile; // as read from the lock file
112 };
113 
KLockFile(const QString & file,const QString & componentName)114 KLockFile::KLockFile(const QString &file, const QString &componentName)
115     : d(new Private(componentName))
116 {
117     d->m_fileName = file;
118 }
119 
~KLockFile()120 KLockFile::~KLockFile()
121 {
122     unlock();
123     delete d;
124 }
125 
126 int
staleTime() const127 KLockFile::staleTime() const
128 {
129     return d->staleTime;
130 }
131 
132 void
setStaleTime(int _staleTime)133 KLockFile::setStaleTime(int _staleTime)
134 {
135     d->staleTime = _staleTime;
136 }
137 
operator ==(const QT_STATBUF & st_buf1,const QT_STATBUF & st_buf2)138 static bool operator==(const QT_STATBUF &st_buf1,
139                        const QT_STATBUF &st_buf2)
140 {
141 #define FIELD_EQ(what)       (st_buf1.what == st_buf2.what)
142     return FIELD_EQ(st_dev) && FIELD_EQ(st_ino) &&
143            FIELD_EQ(st_uid) && FIELD_EQ(st_gid) && FIELD_EQ(st_nlink);
144 #undef FIELD_EQ
145 }
146 
operator !=(const QT_STATBUF & st_buf1,const QT_STATBUF & st_buf2)147 static bool operator!=(const QT_STATBUF &st_buf1,
148                        const QT_STATBUF &st_buf2)
149 {
150     return !(st_buf1 == st_buf2);
151 }
152 
testLinkCountSupport(const QByteArray & fileName)153 static bool testLinkCountSupport(const QByteArray &fileName)
154 {
155     QT_STATBUF st_buf;
156     int result = -1;
157     // Check if hardlinks raise the link count at all?
158     if (!::link(fileName.data(), QByteArray(fileName + ".test").data())) {
159         result = QT_LSTAT(fileName.data(), &st_buf);
160         ::unlink(QByteArray(fileName + ".test").data());
161     }
162     return (result < 0 || ((result == 0) && (st_buf.st_nlink == 2)));
163 }
164 
writeIntoLockFile(QFile & file)165 void KLockFile::Private::writeIntoLockFile(QFile &file)
166 {
167     file.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ReadGroup | QFile::ReadOther);
168 
169     char hostname[256];
170     hostname[0] = 0;
171     gethostname(hostname, 255);
172     hostname[255] = 0;
173     m_hostname = QString::fromLocal8Bit(hostname);
174     if (m_componentName.isEmpty() && QCoreApplication::instance()) { // TODO Qt5: should be fixed by new Q_GLOBAL_STATIC: qcoreappdata() was dangling, in kconfigtest testSyncOnExit.
175         m_componentName = QCoreApplication::applicationName();
176     }
177 
178     m_pid = getpid();
179 
180     file.write(QByteArray::number(m_pid) + '\n');
181     file.write(m_componentName.toUtf8() + '\n');
182     file.write(hostname);
183     file.flush();
184 }
185 
readLockFile()186 void KLockFile::Private::readLockFile()
187 {
188     m_pid = -1;
189     m_hostname.clear();
190     m_componentNameFromFile.clear();
191 
192     QFile file(m_fileName);
193     if (file.open(QIODevice::ReadOnly)) {
194         QTextStream ts(&file);
195         if (!ts.atEnd()) {
196             m_pid = ts.readLine().toInt();
197         }
198         if (!ts.atEnd()) {
199             m_componentNameFromFile = ts.readLine();
200         }
201         if (!ts.atEnd()) {
202             m_hostname = ts.readLine();
203         }
204     }
205 }
206 
lockFileWithLink(QT_STATBUF & st_buf)207 KLockFile::LockResult KLockFile::Private::lockFileWithLink(QT_STATBUF &st_buf)
208 {
209     const QByteArray lockFileName = QFile::encodeName(m_fileName);
210     int result = QT_LSTAT(lockFileName.data(), &st_buf);
211     if (result == 0) {
212         return KLockFile::LockFail;
213     }
214 
215     QTemporaryFile uniqueFile;
216     uniqueFile.setFileTemplate(m_fileName);
217     if (!uniqueFile.open()) {
218         return KLockFile::LockError;
219     }
220 
221     writeIntoLockFile(uniqueFile);
222 
223     QByteArray uniqueName = QFile::encodeName(uniqueFile.fileName());
224 
225     // Create lock file
226     result = ::link(uniqueName.data(), lockFileName.data());
227     if (result != 0) {
228         return KLockFile::LockError;
229     }
230 
231     if (!linkCountSupport) {
232         return KLockFile::LockOK;
233     }
234 
235     QT_STATBUF st_buf2;
236     result = QT_LSTAT(uniqueName.data(), &st_buf2);
237     if (result != 0) {
238         return KLockFile::LockError;
239     }
240 
241     result = QT_LSTAT(lockFileName.data(), &st_buf);
242     if (result != 0) {
243         return KLockFile::LockError;
244     }
245 
246     if (st_buf != st_buf2 || S_ISLNK(st_buf.st_mode) || S_ISLNK(st_buf2.st_mode)) {
247         // SMBFS supports hardlinks by copying the file, as a result the above test will always fail
248         // cifs increases link count artifically but the inodes are still different
249         if ((st_buf2.st_nlink > 1 ||
250                 ((st_buf.st_nlink == 1) && (st_buf2.st_nlink == 1))) && (st_buf.st_ino != st_buf2.st_ino)) {
251             linkCountSupport = testLinkCountSupport(uniqueName);
252             if (!linkCountSupport) {
253                 return KLockFile::LockOK;    // Link count support is missing... assume everything is OK.
254             }
255         }
256         return KLockFile::LockFail;
257     }
258 
259     return KLockFile::LockOK;
260 }
261 
isNfs() const262 bool KLockFile::Private::isNfs() const
263 {
264     const KFileSystemType::Type fsType = KFileSystemType::fileSystemType(m_fileName);
265     return fsType == KFileSystemType::Nfs;
266 }
267 
lockFile(QT_STATBUF & st_buf)268 KLockFile::LockResult KLockFile::Private::lockFile(QT_STATBUF &st_buf)
269 {
270     if (isNfs()) {
271         return lockFileWithLink(st_buf);
272     }
273 
274     return lockFileOExcl(st_buf);
275 }
276 
lockFileOExcl(QT_STATBUF & st_buf)277 KLockFile::LockResult KLockFile::Private::lockFileOExcl(QT_STATBUF &st_buf)
278 {
279     const QByteArray lockFileName = QFile::encodeName(m_fileName);
280 
281     int fd = QT_OPEN(lockFileName.constData(), O_WRONLY | O_CREAT | O_EXCL, 0644);
282     if (fd < 0) {
283         if (errno == EEXIST) {
284             // File already exists
285             if (QT_LSTAT(lockFileName.constData(), &st_buf) != 0) { // caller wants stat buf details
286                 // File got deleted meanwhile! Clear struct rather than leaving it unset.
287                 st_buf.st_dev = 0;
288                 st_buf.st_ino = 0;
289                 st_buf.st_uid = 0;
290                 st_buf.st_gid = 0;
291                 st_buf.st_nlink = 0;
292             }
293             return LockFail;
294         } else {
295             return LockError;
296         }
297     }
298     // We hold the lock, continue.
299     if (!m_file.open(fd, QIODevice::WriteOnly)) {
300         close(fd);
301         return LockError;
302     }
303     writeIntoLockFile(m_file);
304 
305     // stat to get the modification time
306     const int result = QT_LSTAT(QFile::encodeName(m_fileName).data(), &st_buf);
307     if (result != 0) {
308         close(fd);
309         return KLockFile::LockError;
310     }
311     mustCloseFd = true;
312     return KLockFile::LockOK;
313 }
314 
deleteStaleLock()315 KLockFile::LockResult KLockFile::Private::deleteStaleLock()
316 {
317     if (isNfs()) {
318         return deleteStaleLockWithLink();
319     }
320 
321     // I see no way to prevent the race condition here, where we could
322     // delete a new lock file that another process just got after we
323     // decided the old one was too stale for us too.
324     qWarning("WARNING: deleting stale lockfile %s", qPrintable(m_fileName));
325     QFile::remove(m_fileName);
326     return LockOK;
327 }
328 
deleteStaleLockWithLink()329 KLockFile::LockResult KLockFile::Private::deleteStaleLockWithLink()
330 {
331     // This is dangerous, we could be deleting a new lock instead of
332     // the old stale one, let's be very careful
333 
334     // Create temp file
335     QTemporaryFile *ktmpFile = new QTemporaryFile;
336     ktmpFile->setFileTemplate(m_fileName);
337     if (!ktmpFile->open()) {
338         delete ktmpFile;
339         return KLockFile::LockError;
340     }
341 
342     const QByteArray lckFile = QFile::encodeName(m_fileName);
343     const QByteArray tmpFile = QFile::encodeName(ktmpFile->fileName());
344     delete ktmpFile;
345 
346     // link to lock file
347     if (::link(lckFile.data(), tmpFile.data()) != 0) {
348         return KLockFile::LockFail;    // Try again later
349     }
350 
351     // check if link count increased with exactly one
352     // and if the lock file still matches
353     QT_STATBUF st_buf1;
354     QT_STATBUF st_buf2;
355     memcpy(&st_buf1, &statBuf, sizeof(QT_STATBUF));
356     st_buf1.st_nlink++;
357     if ((QT_LSTAT(tmpFile.data(), &st_buf2) == 0) && st_buf1 == st_buf2) {
358         if ((QT_LSTAT(lckFile.data(), &st_buf2) == 0) && st_buf1 == st_buf2) {
359             // - - if yes, delete lock file, delete temp file, retry lock
360             qWarning("WARNING: deleting stale lockfile %s", lckFile.data());
361             ::unlink(lckFile.data());
362             ::unlink(tmpFile.data());
363             return KLockFile::LockOK;
364         }
365     }
366 
367     // SMBFS supports hardlinks by copying the file, as a result the above test will always fail
368     if (linkCountSupport) {
369         linkCountSupport = testLinkCountSupport(tmpFile);
370     }
371 
372     if (!linkCountSupport) {
373         // Without support for link counts we will have a little race condition
374         qWarning("WARNING: deleting stale lockfile %s", lckFile.data());
375         ::unlink(tmpFile.data());
376         if (::unlink(lckFile.data()) < 0) {
377             qWarning("WARNING: Problem deleting stale lockfile %s: %s", lckFile.data(),
378                      strerror(errno));
379             return KLockFile::LockFail;
380         }
381         return KLockFile::LockOK;
382     }
383 
384     // Failed to delete stale lock file
385     qWarning("WARNING: Problem deleting stale lockfile %s", lckFile.data());
386     ::unlink(tmpFile.data());
387     return KLockFile::LockFail;
388 }
389 
lock(LockFlags options)390 KLockFile::LockResult KLockFile::lock(LockFlags options)
391 {
392     if (d->isLocked) {
393         return KLockFile::LockOK;
394     }
395 
396     KLockFile::LockResult result;
397     int hardErrors = 5;
398     int n = 5;
399     while (true) {
400         QT_STATBUF st_buf;
401         // Try to create the lock file
402         result = d->lockFile(st_buf);
403 
404         if (result == KLockFile::LockOK) {
405             d->staleTimer = QTime();
406             break;
407         } else if (result == KLockFile::LockError) {
408             d->staleTimer = QTime();
409             if (--hardErrors == 0) {
410                 break;
411             }
412         } else { // KLockFile::Fail -- there is already such a file present (e.g. left by a crashed app)
413             if (!d->staleTimer.isNull() && d->statBuf != st_buf) {
414                 d->staleTimer = QTime();
415             }
416 
417             if (d->staleTimer.isNull()) {
418                 memcpy(&(d->statBuf), &st_buf, sizeof(QT_STATBUF));
419                 d->staleTimer.start();
420 
421                 d->readLockFile();
422             }
423 
424             bool isStale = false;
425             if ((d->m_pid > 0) && !d->m_hostname.isEmpty()) {
426                 // Check if hostname is us
427                 char hostname[256];
428                 hostname[0] = 0;
429                 gethostname(hostname, 255);
430                 hostname[255] = 0;
431 
432                 if (d->m_hostname == QString::fromLocal8Bit(hostname)) {
433                     // Check if pid still exists
434                     int res = ::kill(d->m_pid, 0);
435                     if ((res == -1) && (errno == ESRCH)) {
436                         isStale = true;    // pid does not exist
437                     }
438                 }
439             }
440             if (d->staleTimer.elapsed() > (d->staleTime * 1000)) {
441                 isStale = true;
442             }
443 
444             if (isStale) {
445                 if ((options & ForceFlag) == 0) {
446                     return KLockFile::LockStale;
447                 }
448 
449                 result = d->deleteStaleLock();
450 
451                 if (result == KLockFile::LockOK) {
452                     // Lock deletion successful
453                     d->staleTimer = QTime();
454                     continue; // Now try to get the new lock
455                 } else if (result != KLockFile::LockFail) {
456                     return result;
457                 }
458             }
459         }
460 
461         if (options & NoBlockFlag) {
462             break;
463         }
464 
465         struct timeval tv;
466         tv.tv_sec = 0;
467         tv.tv_usec = n * ((KRandom::random() % 200) + 100);
468         if (n < 2000) {
469             n = n * 2;
470         }
471 
472         select(0, nullptr, nullptr, nullptr, &tv);
473     }
474     if (result == LockOK) {
475         d->isLocked = true;
476     }
477     return result;
478 }
479 
isLocked() const480 bool KLockFile::isLocked() const
481 {
482     return d->isLocked;
483 }
484 
unlock()485 void KLockFile::unlock()
486 {
487     if (d->isLocked) {
488         ::unlink(QFile::encodeName(d->m_fileName).data());
489         if (d->mustCloseFd) {
490             close(d->m_file.handle());
491             d->mustCloseFd = false;
492         }
493         d->m_file.close();
494         d->m_pid = -1;
495         d->isLocked = false;
496     }
497 }
498 
getLockInfo(int & pid,QString & hostname,QString & appname)499 bool KLockFile::getLockInfo(int &pid, QString &hostname, QString &appname)
500 {
501     if (d->m_pid == -1) {
502         return false;
503     }
504     pid = d->m_pid;
505     hostname = d->m_hostname;
506     appname = d->m_componentNameFromFile;
507     return true;
508 }
509