File indexing completed on 2024-04-28 15:17:37

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2007-2010 Sebastian Trueg <trueg@kde.org>
0004     SPDX-FileCopyrightText: 2012-2014 Vishesh Handa <vhanda@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "kinotify.h"
0010 #include "fileindexerconfig.h"
0011 #include "filtereddiriterator.h"
0012 #include "baloodebug.h"
0013 
0014 #include <QSocketNotifier>
0015 #include <QHash>
0016 #include <QFile>
0017 #include <QTimer>
0018 #include <QDeadlineTimer>
0019 #include <QPair>
0020 
0021 #include <sys/inotify.h>
0022 #include <sys/utsname.h>
0023 #include <sys/types.h>
0024 #include <sys/stat.h>
0025 #include <sys/ioctl.h>
0026 #include <unistd.h>
0027 #include <fcntl.h>
0028 #include <cerrno>
0029 #include <dirent.h>
0030 
0031 namespace
0032 {
0033 QByteArray normalizeTrailingSlash(QByteArray&& path)
0034 {
0035     if (!path.endsWith('/')) {
0036         path.append('/');
0037     }
0038     return path;
0039 }
0040 
0041 QByteArray concatPath(const QByteArray& p1, const QByteArray& p2)
0042 {
0043     QByteArray p(p1);
0044     if (p.isEmpty() || (!p2.isEmpty() && p[p.length() - 1] != '/')) {
0045         p.append('/');
0046     }
0047     p.append(p2);
0048     return p;
0049 }
0050 }
0051 
0052 class KInotify::Private
0053 {
0054 public:
0055     Private(KInotify* parent)
0056         : userLimitReachedSignaled(false)
0057         , config(nullptr)
0058         , m_inotifyFd(-1)
0059         , m_notifier(nullptr)
0060         , q(parent) {
0061     }
0062 
0063     ~Private() {
0064         close();
0065     }
0066 
0067     struct MovedFileCookie {
0068         QDeadlineTimer deadline;
0069         QByteArray path;
0070         WatchFlags flags;
0071     };
0072 
0073     QHash<int, MovedFileCookie> cookies;
0074     QTimer cookieExpireTimer;
0075     // This variable is set to true if the watch limit is reached, and reset when it is raised
0076     bool userLimitReachedSignaled;
0077 
0078     // url <-> wd mappings
0079     QHash<int, QByteArray> watchPathHash;
0080     QHash<QByteArray, int> pathWatchHash;
0081 
0082     bool parentWatched(const QByteArray& path)
0083     {
0084         auto parent = path.chopped(1);
0085         if (auto index = parent.lastIndexOf('/'); index > 0) {
0086             parent.truncate(index + 1);
0087             return pathWatchHash.contains(parent);
0088         }
0089         return false;
0090     }
0091 
0092     Baloo::FileIndexerConfig* config;
0093     QStringList m_paths;
0094     std::unique_ptr<Baloo::FilteredDirIterator> m_dirIter;
0095 
0096     // FIXME: only stored from the last addWatch call
0097     WatchEvents mode;
0098     WatchFlags flags;
0099 
0100     int inotify() {
0101         if (m_inotifyFd < 0) {
0102             open();
0103         }
0104         return m_inotifyFd;
0105     }
0106 
0107     void close() {
0108         delete m_notifier;
0109         m_notifier = nullptr;
0110 
0111         ::close(m_inotifyFd);
0112         m_inotifyFd = -1;
0113     }
0114 
0115     bool addWatch(const QString& path) {
0116         WatchEvents newMode = mode;
0117         WatchFlags newFlags = flags;
0118 
0119         // we always need the unmount event to maintain our path hash
0120         const int mask = newMode | newFlags | EventUnmount | FlagExclUnlink;
0121 
0122         const QByteArray encpath = normalizeTrailingSlash(QFile::encodeName(path));
0123         int wd = inotify_add_watch(inotify(), encpath.data(), mask);
0124         if (wd > 0) {
0125 //             qCDebug(BALOO) << "Successfully added watch for" << path << watchPathHash.count();
0126             watchPathHash.insert(wd, encpath);
0127             pathWatchHash.insert(encpath, wd);
0128             return true;
0129         } else {
0130             qCDebug(BALOO) << "Failed to create watch for" << path << strerror(errno);
0131             //If we could not create the watch because we have hit the limit, try raising it.
0132             if (errno == ENOSPC) {
0133                 //If we can't, fall back to signalling
0134                 qCDebug(BALOO) << "User limit reached. Count: " << watchPathHash.count();
0135                 userLimitReachedSignaled = true;
0136                 Q_EMIT q->watchUserLimitReached(path);
0137             }
0138             return false;
0139         }
0140     }
0141 
0142     void removeWatch(int wd) {
0143         pathWatchHash.remove(watchPathHash.take(wd));
0144         inotify_rm_watch(inotify(), wd);
0145     }
0146 
0147     /**
0148      * Add one watch and call oneself asynchronously
0149      */
0150     void _k_addWatches()
0151     {
0152         // It is much faster to add watches in batches instead of adding each one
0153         // asynchronously. Try out the inotify test to compare results.
0154         for (int i = 0; i < 1000; i++) {
0155             if (userLimitReachedSignaled) {
0156                 return;
0157             }
0158             if (!m_dirIter || m_dirIter->next().isEmpty()) {
0159                 if (!m_paths.isEmpty()) {
0160                     m_dirIter = std::make_unique<Baloo::FilteredDirIterator>(config, m_paths.takeFirst(), Baloo::FilteredDirIterator::DirsOnly);
0161                 } else {
0162                     m_dirIter = nullptr;
0163                     break;
0164                 }
0165             } else {
0166                 addWatch(m_dirIter->filePath());
0167             }
0168         }
0169 
0170         // asynchronously add the next batch
0171         if (m_dirIter) {
0172             QMetaObject::invokeMethod(q, [this]() {
0173                 this->_k_addWatches();
0174             }, Qt::QueuedConnection);
0175         }
0176         else {
0177             Q_EMIT q->installedWatches();
0178         }
0179     }
0180 
0181 private:
0182     void open() {
0183         m_inotifyFd = inotify_init();
0184         delete m_notifier;
0185         if (m_inotifyFd > 0) {
0186             fcntl(m_inotifyFd, F_SETFD, FD_CLOEXEC);
0187             m_notifier = new QSocketNotifier(m_inotifyFd, QSocketNotifier::Read);
0188             connect(m_notifier, &QSocketNotifier::activated, q, &KInotify::slotEvent);
0189         } else {
0190             Q_ASSERT_X(0, "kinotify", "Failed to initialize inotify");
0191         }
0192     }
0193 
0194     int m_inotifyFd;
0195     QSocketNotifier* m_notifier;
0196 
0197     KInotify* q;
0198 };
0199 
0200 KInotify::KInotify(Baloo::FileIndexerConfig* config, QObject* parent)
0201     : QObject(parent)
0202     , d(new Private(this))
0203 {
0204     d->config = config;
0205     // 1 second is more than enough time for the EventMoveTo event to occur
0206     // after the EventMoveFrom event has occurred
0207     d->cookieExpireTimer.setInterval(1000);
0208     d->cookieExpireTimer.setSingleShot(true);
0209     connect(&d->cookieExpireTimer, &QTimer::timeout, this, &KInotify::slotClearCookies);
0210 }
0211 
0212 KInotify::~KInotify()
0213 {
0214     delete d;
0215 }
0216 
0217 bool KInotify::watchingPath(const QString& path) const
0218 {
0219     const QByteArray p = normalizeTrailingSlash(QFile::encodeName(path));
0220     return d->pathWatchHash.contains(p);
0221 }
0222 
0223 void KInotify::resetUserLimit()
0224 {
0225     d->userLimitReachedSignaled = false;
0226 }
0227 
0228 bool KInotify::addWatch(const QString& path, WatchEvents mode, WatchFlags flags)
0229 {
0230 //    qCDebug(BALOO) << path;
0231 
0232     d->mode = mode;
0233     d->flags = flags;
0234     d->m_paths << path;
0235     // If the inotify user limit has been signaled,
0236     // there will be no watchInstalled signal
0237     if (d->userLimitReachedSignaled) {
0238         return false;
0239     }
0240 
0241     QMetaObject::invokeMethod(this, [this]() {
0242         this->d->_k_addWatches();
0243     }, Qt::QueuedConnection);
0244     return true;
0245 }
0246 
0247 bool KInotify::removeWatch(const QString& path)
0248 {
0249     // Stop all of the iterators which contain path
0250     QMutableListIterator<QString> iter(d->m_paths);
0251     while (iter.hasNext()) {
0252         if (iter.next().startsWith(path)) {
0253             iter.remove();
0254         }
0255     }
0256     if (d->m_dirIter) {
0257         if (d->m_dirIter->filePath().startsWith(path)) {
0258             d->m_dirIter = nullptr;
0259         }
0260     }
0261 
0262     // Remove all the watches
0263     QByteArray encodedPath(QFile::encodeName(path));
0264     auto it = d->watchPathHash.begin();
0265     while (it != d->watchPathHash.end()) {
0266         if (it.value().startsWith(encodedPath)) {
0267             inotify_rm_watch(d->inotify(), it.key());
0268             d->pathWatchHash.remove(it.value());
0269             it = d->watchPathHash.erase(it);
0270         } else {
0271             ++it;
0272         }
0273     }
0274     return true;
0275 }
0276 
0277 void KInotify::handleDirCreated(const QString& path)
0278 {
0279     Baloo::FilteredDirIterator it(d->config, path);
0280     // First entry is the directory itself (if not excluded)
0281     if (!it.next().isEmpty()) {
0282         d->addWatch(it.filePath());
0283     }
0284     while (!it.next().isEmpty()) {
0285         Q_EMIT created(it.filePath(), it.fileInfo().isDir());
0286         if (it.fileInfo().isDir()) {
0287             d->addWatch(it.filePath());
0288         }
0289     }
0290 }
0291 
0292 void KInotify::slotEvent(int socket)
0293 {
0294     int avail;
0295     if (ioctl(socket, FIONREAD, &avail) == EINVAL) {
0296         qCDebug(BALOO) << "Did not receive an entire inotify event.";
0297         return;
0298     }
0299 
0300     char* buffer = (char*)malloc(avail);
0301 
0302     const int len = read(socket, buffer, avail);
0303     Q_ASSERT(len == avail);
0304 
0305     // deadline for MoveFrom events without matching MoveTo event
0306     QDeadlineTimer deadline(QDeadlineTimer::Forever);
0307 
0308     int i = 0;
0309     while (i < len) {
0310         const struct inotify_event* event = (struct inotify_event*)&buffer[i];
0311 
0312         QByteArray path;
0313 
0314         // Overflow happens sometimes if we process the events too slowly
0315         if (event->wd < 0 && (event->mask & EventQueueOverflow)) {
0316             qCWarning(BALOO) << "Inotify - too many event - Overflowed";
0317             free(buffer);
0318             return;
0319         }
0320 
0321         // the event name only contains an interesting value if we get an event for a file/folder inside
0322         // a watched folder. Otherwise we should ignore it
0323         if (event->mask & (EventDeleteSelf | EventMoveSelf)) {
0324             path = d->watchPathHash.value(event->wd);
0325         } else {
0326             // we cannot use event->len here since it contains the size of the buffer and not the length of the string
0327             const QByteArray eventName = QByteArray::fromRawData(event->name, qstrnlen(event->name, event->len));
0328             const QByteArray hashedPath = d->watchPathHash.value(event->wd);
0329             path = concatPath(hashedPath, eventName);
0330             if (event->mask & IN_ISDIR) {
0331                 path = normalizeTrailingSlash(std::move(path));
0332             }
0333         }
0334 
0335         Q_ASSERT(!path.isEmpty() || event->mask & EventIgnored);
0336         Q_ASSERT(path != "/" || event->mask & EventIgnored  || event->mask & EventUnmount);
0337 
0338         // All events which need a decoded path, i.e. everything
0339         // but EventMoveFrom | EventQueueOverflow | EventIgnored
0340         uint32_t fileEvents = EventAll & ~(EventMoveFrom | EventQueueOverflow | EventIgnored);
0341         const QString fname = (event->mask & fileEvents) ? QFile::decodeName(path) : QString();
0342 
0343         // now signal the event
0344         if (event->mask & EventAccess) {
0345 //            qCDebug(BALOO) << path << "EventAccess";
0346             Q_EMIT accessed(fname);
0347         }
0348         if (event->mask & EventAttributeChange) {
0349 //            qCDebug(BALOO) << path << "EventAttributeChange";
0350             Q_EMIT attributeChanged(fname);
0351         }
0352         if (event->mask & EventCloseWrite) {
0353 //            qCDebug(BALOO) << path << "EventCloseWrite";
0354             Q_EMIT closedWrite(fname);
0355         }
0356         if (event->mask & EventCloseRead) {
0357 //            qCDebug(BALOO) << path << "EventCloseRead";
0358             Q_EMIT closedRead(fname);
0359         }
0360         if (event->mask & EventCreate) {
0361 //            qCDebug(BALOO) << path << "EventCreate";
0362             Q_EMIT created(fname, event->mask & IN_ISDIR);
0363             if (event->mask & IN_ISDIR) {
0364                 // Files/directories inside the new directory may be created before the watch
0365                 // is installed. Ensure created events for all children are issued at least once
0366                 handleDirCreated(fname);
0367             }
0368         }
0369         if (event->mask & EventDeleteSelf) {
0370 //            qCDebug(BALOO) << path << "EventDeleteSelf";
0371             d->removeWatch(event->wd);
0372             Q_EMIT deleted(fname, true);
0373         }
0374         if (event->mask & EventDelete) {
0375 //            qCDebug(BALOO) << path << "EventDelete";
0376             // we watch all folders recursively. Thus, folder removing is reported in DeleteSelf.
0377             if (!(event->mask & IN_ISDIR)) {
0378                 Q_EMIT deleted(fname, false);
0379             }
0380         }
0381         if (event->mask & EventModify) {
0382 //            qCDebug(BALOO) << path << "EventModify";
0383             Q_EMIT modified(fname);
0384         }
0385         if (event->mask & EventMoveSelf) {
0386 //            qCDebug(BALOO) << path << "EventMoveSelf";
0387             // Problematic if the parent is not watched, otherwise
0388             // handled by MoveFrom/MoveTo from the parent
0389             if (!d->parentWatched(path))
0390                 qCWarning(BALOO) << path << "EventMoveSelf: THIS CASE MAY NOT BE HANDLED PROPERLY!";
0391         }
0392         if (event->mask & EventMoveFrom) {
0393 //            qCDebug(BALOO) << path << "EventMoveFrom";
0394             if (deadline.isForever()) {
0395                 deadline = QDeadlineTimer(1000); // 1 second
0396             }
0397             d->cookies[event->cookie] = Private::MovedFileCookie{ deadline, path, WatchFlags(event->mask) };
0398         }
0399         if (event->mask & EventMoveTo) {
0400             // check if we have a cookie for this one
0401             if (d->cookies.contains(event->cookie)) {
0402                 const QByteArray oldPath = d->cookies.take(event->cookie).path;
0403 
0404                 // update the path cache
0405                 if (event->mask & IN_ISDIR) {
0406                     auto it = d->pathWatchHash.find(oldPath);
0407                     if (it != d->pathWatchHash.end()) {
0408 //                        qCDebug(BALOO) << oldPath << path;
0409                         const int wd = it.value();
0410                         d->watchPathHash[wd] = path;
0411                         d->pathWatchHash.erase(it);
0412                         d->pathWatchHash.insert(path, wd);
0413                     }
0414                 }
0415 //                qCDebug(BALOO) << oldPath << "EventMoveTo" << path;
0416                 Q_EMIT moved(QFile::decodeName(oldPath), fname);
0417             } else {
0418 //                qCDebug(BALOO) << "No cookie for move information of" << path << "simulating new file event";
0419                 Q_EMIT created(fname, event->mask & IN_ISDIR);
0420                 if (event->mask & IN_ISDIR) {
0421                     handleDirCreated(fname);
0422                 }
0423             }
0424         }
0425         if (event->mask & EventOpen) {
0426 //            qCDebug(BALOO) << path << "EventOpen";
0427             Q_EMIT opened(fname);
0428         }
0429         if (event->mask & EventUnmount) {
0430 //            qCDebug(BALOO) << path << "EventUnmount. removing from path hash";
0431             if (event->mask & IN_ISDIR) {
0432                 d->removeWatch(event->wd);
0433             }
0434             // This is present because a unmount event is sent by inotify after unmounting, by
0435             // which time the watches have already been removed.
0436             if (path != "/") {
0437                 Q_EMIT unmounted(fname);
0438             }
0439         }
0440         if (event->mask & EventIgnored) {
0441 //             qCDebug(BALOO) << path << "EventIgnored";
0442         }
0443 
0444         i += sizeof(struct inotify_event) + event->len;
0445     }
0446 
0447     if (d->cookies.empty()) {
0448         d->cookieExpireTimer.stop();
0449     } else {
0450         if (!d->cookieExpireTimer.isActive()) {
0451             d->cookieExpireTimer.start();
0452         }
0453     }
0454 
0455     if (len < 0) {
0456         qCDebug(BALOO) << "Failed to read event.";
0457     }
0458 
0459     free(buffer);
0460 }
0461 
0462 void KInotify::slotClearCookies()
0463 {
0464     auto now = QDeadlineTimer::current();
0465 
0466     auto it = d->cookies.begin();
0467     while (it != d->cookies.end()) {
0468         if (now > (*it).deadline) {
0469             const QString fname = QFile::decodeName((*it).path);
0470             removeWatch(fname);
0471             Q_EMIT deleted(fname, (*it).flags & IN_ISDIR);
0472             it = d->cookies.erase(it);
0473         } else {
0474             ++it;
0475         }
0476     }
0477 
0478     if (!d->cookies.empty()) {
0479         d->cookieExpireTimer.start();
0480     }
0481 }
0482 
0483 #include "moc_kinotify.cpp"