File indexing completed on 2024-05-05 03:52:15
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"