1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2007-2010 Sebastian Trueg <trueg@kde.org>
4 SPDX-FileCopyrightText: 2012-2014 Vishesh Handa <vhanda@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "kinotify.h"
10#include "fileindexerconfig.h"
11#include "filtereddiriterator.h"
12#include "baloodebug.h"
13
14#include <QSocketNotifier>
15#include <QHash>
16#include <QFile>
17#include <QTimer>
18#include <QDeadlineTimer>
19#include <QPair>
20
21#include <sys/inotify.h>
22#include <sys/utsname.h>
23#include <sys/types.h>
24#include <sys/stat.h>
25#include <sys/ioctl.h>
26#include <unistd.h>
27#include <fcntl.h>
28#include <cerrno>
29#include <dirent.h>
30
31namespace
32{
33QByteArray normalizeTrailingSlash(QByteArray&& path)
34{
35 if (!path.endsWith(c: '/')) {
36 path.append(c: '/');
37 }
38 return path;
39}
40
41QByteArray concatPath(const QByteArray& p1, const QByteArray& p2)
42{
43 QByteArray p(p1);
44 if (p.isEmpty() || (!p2.isEmpty() && p[p.length() - 1] != '/')) {
45 p.append(c: '/');
46 }
47 p.append(a: p2);
48 return p;
49}
50}
51
52class KInotify::Private
53{
54public:
55 Private(KInotify* parent)
56 : userLimitReachedSignaled(false)
57 , config(nullptr)
58 , m_inotifyFd(-1)
59 , m_notifier(nullptr)
60 , q(parent) {
61 }
62
63 ~Private() {
64 close();
65 }
66
67 struct MovedFileCookie {
68 QDeadlineTimer deadline;
69 QByteArray path;
70 WatchFlags flags;
71 };
72
73 QHash<int, MovedFileCookie> cookies;
74 QTimer cookieExpireTimer;
75 // This variable is set to true if the watch limit is reached, and reset when it is raised
76 bool userLimitReachedSignaled;
77
78 // url <-> wd mappings
79 QHash<int, QByteArray> watchPathHash;
80 QHash<QByteArray, int> pathWatchHash;
81
82 bool parentWatched(const QByteArray& path)
83 {
84 auto parent = path.chopped(len: 1);
85 if (auto index = parent.lastIndexOf(c: '/'); index > 0) {
86 parent.truncate(pos: index + 1);
87 return pathWatchHash.contains(key: parent);
88 }
89 return false;
90 }
91
92 Baloo::FileIndexerConfig* config;
93 QStringList m_paths;
94 std::unique_ptr<Baloo::FilteredDirIterator> m_dirIter;
95
96 // FIXME: only stored from the last addWatch call
97 WatchEvents mode;
98 WatchFlags flags;
99
100 int inotify() {
101 if (m_inotifyFd < 0) {
102 open();
103 }
104 return m_inotifyFd;
105 }
106
107 void close() {
108 delete m_notifier;
109 m_notifier = nullptr;
110
111 ::close(fd: m_inotifyFd);
112 m_inotifyFd = -1;
113 }
114
115 bool addWatch(const QString& path) {
116 WatchEvents newMode = mode;
117 WatchFlags newFlags = flags;
118
119 // we always need the unmount event to maintain our path hash
120 const int mask = newMode | newFlags | EventUnmount | FlagExclUnlink;
121
122 const QByteArray encpath = normalizeTrailingSlash(path: QFile::encodeName(fileName: path));
123 int wd = inotify_add_watch(fd: inotify(), name: encpath.data(), mask: mask);
124 if (wd > 0) {
125// qCDebug(BALOO) << "Successfully added watch for" << path << watchPathHash.count();
126 watchPathHash.insert(key: wd, value: encpath);
127 pathWatchHash.insert(key: encpath, value: wd);
128 return true;
129 } else {
130 qCDebug(BALOO) << "Failed to create watch for" << path << strerror(errno);
131 //If we could not create the watch because we have hit the limit, try raising it.
132 if (errno == ENOSPC) {
133 //If we can't, fall back to signalling
134 qCDebug(BALOO) << "User limit reached. Count: " << watchPathHash.count();
135 userLimitReachedSignaled = true;
136 Q_EMIT q->watchUserLimitReached(path);
137 }
138 return false;
139 }
140 }
141
142 void removeWatch(int wd) {
143 pathWatchHash.remove(key: watchPathHash.take(key: wd));
144 inotify_rm_watch(fd: inotify(), wd: wd);
145 }
146
147 /**
148 * Add one watch and call oneself asynchronously
149 */
150 void _k_addWatches()
151 {
152 // It is much faster to add watches in batches instead of adding each one
153 // asynchronously. Try out the inotify test to compare results.
154 for (int i = 0; i < 1000; i++) {
155 if (userLimitReachedSignaled) {
156 return;
157 }
158 if (!m_dirIter || m_dirIter->next().isEmpty()) {
159 if (!m_paths.isEmpty()) {
160 m_dirIter = std::make_unique<Baloo::FilteredDirIterator>(args&: config, args: m_paths.takeFirst(), args: Baloo::FilteredDirIterator::DirsOnly);
161 } else {
162 m_dirIter = nullptr;
163 break;
164 }
165 } else {
166 addWatch(path: m_dirIter->filePath());
167 }
168 }
169
170 // asynchronously add the next batch
171 if (m_dirIter) {
172 QMetaObject::invokeMethod(object: q, function: [this]() {
173 this->_k_addWatches();
174 }, type: Qt::QueuedConnection);
175 }
176 else {
177 Q_EMIT q->installedWatches();
178 }
179 }
180
181private:
182 void open() {
183 m_inotifyFd = inotify_init();
184 delete m_notifier;
185 if (m_inotifyFd > 0) {
186 fcntl(fd: m_inotifyFd, F_SETFD, FD_CLOEXEC);
187 m_notifier = new QSocketNotifier(m_inotifyFd, QSocketNotifier::Read);
188 connect(sender: m_notifier, signal: &QSocketNotifier::activated, context: q, slot: &KInotify::slotEvent);
189 } else {
190 Q_ASSERT_X(0, "kinotify", "Failed to initialize inotify");
191 }
192 }
193
194 int m_inotifyFd;
195 QSocketNotifier* m_notifier;
196
197 KInotify* q;
198};
199
200KInotify::KInotify(Baloo::FileIndexerConfig* config, QObject* parent)
201 : QObject(parent)
202 , d(new Private(this))
203{
204 d->config = config;
205 // 1 second is more than enough time for the EventMoveTo event to occur
206 // after the EventMoveFrom event has occurred
207 d->cookieExpireTimer.setInterval(1000);
208 d->cookieExpireTimer.setSingleShot(true);
209 connect(sender: &d->cookieExpireTimer, signal: &QTimer::timeout, context: this, slot: &KInotify::slotClearCookies);
210}
211
212KInotify::~KInotify()
213{
214 delete d;
215}
216
217bool KInotify::watchingPath(const QString& path) const
218{
219 const QByteArray p = normalizeTrailingSlash(path: QFile::encodeName(fileName: path));
220 return d->pathWatchHash.contains(key: p);
221}
222
223void KInotify::resetUserLimit()
224{
225 d->userLimitReachedSignaled = false;
226}
227
228bool KInotify::addWatch(const QString& path, WatchEvents mode, WatchFlags flags)
229{
230// qCDebug(BALOO) << path;
231
232 d->mode = mode;
233 d->flags = flags;
234 d->m_paths << path;
235 // If the inotify user limit has been signaled,
236 // there will be no watchInstalled signal
237 if (d->userLimitReachedSignaled) {
238 return false;
239 }
240
241 QMetaObject::invokeMethod(object: this, function: [this]() {
242 this->d->_k_addWatches();
243 }, type: Qt::QueuedConnection);
244 return true;
245}
246
247bool KInotify::removeWatch(const QString& path)
248{
249 // Stop all of the iterators which contain path
250 QMutableListIterator<QString> iter(d->m_paths);
251 while (iter.hasNext()) {
252 if (iter.next().startsWith(s: path)) {
253 iter.remove();
254 }
255 }
256 if (d->m_dirIter) {
257 if (d->m_dirIter->filePath().startsWith(s: path)) {
258 d->m_dirIter = nullptr;
259 }
260 }
261
262 // Remove all the watches
263 QByteArray encodedPath(QFile::encodeName(fileName: path));
264 auto it = d->watchPathHash.begin();
265 while (it != d->watchPathHash.end()) {
266 if (it.value().startsWith(bv: encodedPath)) {
267 inotify_rm_watch(fd: d->inotify(), wd: it.key());
268 d->pathWatchHash.remove(key: it.value());
269 it = d->watchPathHash.erase(it);
270 } else {
271 ++it;
272 }
273 }
274 return true;
275}
276
277void KInotify::handleDirCreated(const QString& path)
278{
279 Baloo::FilteredDirIterator it(d->config, path);
280 // First entry is the directory itself (if not excluded)
281 if (!it.next().isEmpty()) {
282 d->addWatch(path: it.filePath());
283 }
284 while (!it.next().isEmpty()) {
285 Q_EMIT created(file: it.filePath(), isDir: it.fileInfo().isDir());
286 if (it.fileInfo().isDir()) {
287 d->addWatch(path: it.filePath());
288 }
289 }
290}
291
292void KInotify::slotEvent(int socket)
293{
294 int avail;
295 if (ioctl(fd: socket, FIONREAD, &avail) == EINVAL) {
296 qCDebug(BALOO) << "Did not receive an entire inotify event.";
297 return;
298 }
299
300 char* buffer = (char*)malloc(size: avail);
301
302 const int len = read(fd: socket, buf: buffer, nbytes: avail);
303 Q_ASSERT(len == avail);
304
305 // deadline for MoveFrom events without matching MoveTo event
306 QDeadlineTimer deadline(QDeadlineTimer::Forever);
307
308 int i = 0;
309 while (i < len) {
310 const struct inotify_event* event = (struct inotify_event*)&buffer[i];
311
312 QByteArray path;
313
314 // Overflow happens sometimes if we process the events too slowly
315 if (event->wd < 0 && (event->mask & EventQueueOverflow)) {
316 qCWarning(BALOO) << "Inotify - too many event - Overflowed";
317 free(ptr: buffer);
318 return;
319 }
320
321 // the event name only contains an interesting value if we get an event for a file/folder inside
322 // a watched folder. Otherwise we should ignore it
323 if (event->mask & (EventDeleteSelf | EventMoveSelf)) {
324 path = d->watchPathHash.value(key: event->wd);
325 } else {
326 // we cannot use event->len here since it contains the size of the buffer and not the length of the string
327 const QByteArray eventName = QByteArray::fromRawData(data: event->name, size: qstrnlen(str: event->name, maxlen: event->len));
328 const QByteArray hashedPath = d->watchPathHash.value(key: event->wd);
329 path = concatPath(p1: hashedPath, p2: eventName);
330 if (event->mask & IN_ISDIR) {
331 path = normalizeTrailingSlash(path: std::move(path));
332 }
333 }
334
335 Q_ASSERT(!path.isEmpty() || event->mask & EventIgnored);
336 Q_ASSERT(path != "/" || event->mask & EventIgnored || event->mask & EventUnmount);
337
338 // All events which need a decoded path, i.e. everything
339 // but EventMoveFrom | EventQueueOverflow | EventIgnored
340 uint32_t fileEvents = EventAll & ~(EventMoveFrom | EventQueueOverflow | EventIgnored);
341 const QString fname = (event->mask & fileEvents) ? QFile::decodeName(localFileName: path) : QString();
342
343 // now signal the event
344 if (event->mask & EventAccess) {
345// qCDebug(BALOO) << path << "EventAccess";
346 Q_EMIT accessed(file: fname);
347 }
348 if (event->mask & EventAttributeChange) {
349// qCDebug(BALOO) << path << "EventAttributeChange";
350 Q_EMIT attributeChanged(file: fname);
351 }
352 if (event->mask & EventCloseWrite) {
353// qCDebug(BALOO) << path << "EventCloseWrite";
354 Q_EMIT closedWrite(file: fname);
355 }
356 if (event->mask & EventCloseRead) {
357// qCDebug(BALOO) << path << "EventCloseRead";
358 Q_EMIT closedRead(file: fname);
359 }
360 if (event->mask & EventCreate) {
361// qCDebug(BALOO) << path << "EventCreate";
362 Q_EMIT created(file: fname, isDir: event->mask & IN_ISDIR);
363 if (event->mask & IN_ISDIR) {
364 // Files/directories inside the new directory may be created before the watch
365 // is installed. Ensure created events for all children are issued at least once
366 handleDirCreated(path: fname);
367 }
368 }
369 if (event->mask & EventDeleteSelf) {
370// qCDebug(BALOO) << path << "EventDeleteSelf";
371 d->removeWatch(wd: event->wd);
372 Q_EMIT deleted(file: fname, isDir: true);
373 }
374 if (event->mask & EventDelete) {
375// qCDebug(BALOO) << path << "EventDelete";
376 // we watch all folders recursively. Thus, folder removing is reported in DeleteSelf.
377 if (!(event->mask & IN_ISDIR)) {
378 Q_EMIT deleted(file: fname, isDir: false);
379 }
380 }
381 if (event->mask & EventModify) {
382// qCDebug(BALOO) << path << "EventModify";
383 Q_EMIT modified(file: fname);
384 }
385 if (event->mask & EventMoveSelf) {
386// qCDebug(BALOO) << path << "EventMoveSelf";
387 // Problematic if the parent is not watched, otherwise
388 // handled by MoveFrom/MoveTo from the parent
389 if (!d->parentWatched(path))
390 qCWarning(BALOO) << path << "EventMoveSelf: THIS CASE MAY NOT BE HANDLED PROPERLY!";
391 }
392 if (event->mask & EventMoveFrom) {
393// qCDebug(BALOO) << path << "EventMoveFrom";
394 if (deadline.isForever()) {
395 deadline = QDeadlineTimer(1000); // 1 second
396 }
397 d->cookies[event->cookie] = Private::MovedFileCookie{ .deadline: deadline, .path: path, .flags: WatchFlags(event->mask) };
398 }
399 if (event->mask & EventMoveTo) {
400 // check if we have a cookie for this one
401 if (d->cookies.contains(key: event->cookie)) {
402 const QByteArray oldPath = d->cookies.take(key: event->cookie).path;
403
404 // update the path cache
405 if (event->mask & IN_ISDIR) {
406 auto it = d->pathWatchHash.find(key: oldPath);
407 if (it != d->pathWatchHash.end()) {
408// qCDebug(BALOO) << oldPath << path;
409 const int wd = it.value();
410 d->watchPathHash[wd] = path;
411 d->pathWatchHash.erase(it);
412 d->pathWatchHash.insert(key: path, value: wd);
413 }
414 }
415// qCDebug(BALOO) << oldPath << "EventMoveTo" << path;
416 Q_EMIT moved(oldName: QFile::decodeName(localFileName: oldPath), newName: fname);
417 } else {
418// qCDebug(BALOO) << "No cookie for move information of" << path << "simulating new file event";
419 Q_EMIT created(file: fname, isDir: event->mask & IN_ISDIR);
420 if (event->mask & IN_ISDIR) {
421 handleDirCreated(path: fname);
422 }
423 }
424 }
425 if (event->mask & EventOpen) {
426// qCDebug(BALOO) << path << "EventOpen";
427 Q_EMIT opened(file: fname);
428 }
429 if (event->mask & EventUnmount) {
430// qCDebug(BALOO) << path << "EventUnmount. removing from path hash";
431 if (event->mask & IN_ISDIR) {
432 d->removeWatch(wd: event->wd);
433 }
434 // This is present because a unmount event is sent by inotify after unmounting, by
435 // which time the watches have already been removed.
436 if (path != "/") {
437 Q_EMIT unmounted(file: fname);
438 }
439 }
440 if (event->mask & EventIgnored) {
441// qCDebug(BALOO) << path << "EventIgnored";
442 }
443
444 i += sizeof(struct inotify_event) + event->len;
445 }
446
447 if (d->cookies.empty()) {
448 d->cookieExpireTimer.stop();
449 } else {
450 if (!d->cookieExpireTimer.isActive()) {
451 d->cookieExpireTimer.start();
452 }
453 }
454
455 if (len < 0) {
456 qCDebug(BALOO) << "Failed to read event.";
457 }
458
459 free(ptr: buffer);
460}
461
462void KInotify::slotClearCookies()
463{
464 auto now = QDeadlineTimer::current();
465
466 auto it = d->cookies.begin();
467 while (it != d->cookies.end()) {
468 if (now > (*it).deadline) {
469 const QString fname = QFile::decodeName(localFileName: (*it).path);
470 removeWatch(path: fname);
471 Q_EMIT deleted(file: fname, isDir: (*it).flags & IN_ISDIR);
472 it = d->cookies.erase(it);
473 } else {
474 ++it;
475 }
476 }
477
478 if (!d->cookies.empty()) {
479 d->cookieExpireTimer.start();
480 }
481}
482
483#include "moc_kinotify.cpp"
484

source code of baloo/src/file/kinotify.cpp