Baloo

kinotify.cpp
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('/')) {
36 path.append('/');
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('/');
46 }
47 p.append(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
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(1);
85 if (auto index = parent.lastIndexOf('/'); index > 0) {
86 parent.truncate(index + 1);
87 return pathWatchHash.contains(parent);
88 }
89 return false;
90 }
91
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(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(QFile::encodeName(path));
123 int wd = inotify_add_watch(inotify(), encpath.data(), mask);
124 if (wd > 0) {
125// qCDebug(BALOO) << "Successfully added watch for" << path << watchPathHash.count();
126 watchPathHash.insert(wd, encpath);
127 pathWatchHash.insert(encpath, 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;
137 }
138 return false;
139 }
140 }
141
142 void removeWatch(int wd) {
143 pathWatchHash.remove(watchPathHash.take(wd));
144 inotify_rm_watch(inotify(), 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>(config, m_paths.takeFirst(), Baloo::FilteredDirIterator::DirsOnly);
161 } else {
162 m_dirIter = nullptr;
163 break;
164 }
165 } else {
166 addWatch(m_dirIter->filePath());
167 }
168 }
169
170 // asynchronously add the next batch
171 if (m_dirIter) {
172 QMetaObject::invokeMethod(q, [this]() {
173 this->_k_addWatches();
175 }
176 else {
178 }
179 }
180
181private:
182 void open() {
183 m_inotifyFd = inotify_init();
184 delete m_notifier;
185 if (m_inotifyFd > 0) {
186 fcntl(m_inotifyFd, F_SETFD, FD_CLOEXEC);
187 m_notifier = new QSocketNotifier(m_inotifyFd, QSocketNotifier::Read);
188 connect(m_notifier, &QSocketNotifier::activated, q, &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(&d->cookieExpireTimer, &QTimer::timeout, this, &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(QFile::encodeName(path));
220 return d->pathWatchHash.contains(p);
221}
222
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(this, [this]() {
242 this->d->_k_addWatches();
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(path)) {
253 iter.remove();
254 }
255 }
256 if (d->m_dirIter) {
257 if (d->m_dirIter->filePath().startsWith(path)) {
258 d->m_dirIter = nullptr;
259 }
260 }
261
262 // Remove all the watches
263 QByteArray encodedPath(QFile::encodeName(path));
264 auto it = d->watchPathHash.begin();
265 while (it != d->watchPathHash.end()) {
266 if (it.value().startsWith(encodedPath)) {
267 inotify_rm_watch(d->inotify(), it.key());
268 d->pathWatchHash.remove(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(it.filePath());
283 }
284 while (!it.next().isEmpty()) {
285 Q_EMIT created(it.filePath(), it.fileInfo().isDir());
286 if (it.fileInfo().isDir()) {
287 d->addWatch(it.filePath());
288 }
289 }
290}
291
292void KInotify::slotEvent(int socket)
293{
294 int avail;
295 if (ioctl(socket, FIONREAD, &avail) == EINVAL) {
296 qCDebug(BALOO) << "Did not receive an entire inotify event.";
297 return;
298 }
299
300 char* buffer = (char*)malloc(avail);
301
302 const int len = read(socket, buffer, avail);
303 Q_ASSERT(len == avail);
304
305 // deadline for MoveFrom events without matching MoveTo event
307
308 int i = 0;
309 while (i < len) {
310 const struct inotify_event* event = (struct inotify_event*)&buffer[i];
311
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(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(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(event->name, qstrnlen(event->name, event->len));
328 const QByteArray hashedPath = d->watchPathHash.value(event->wd);
329 path = concatPath(hashedPath, eventName);
330 if (event->mask & IN_ISDIR) {
331 path = normalizeTrailingSlash(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(path) : QString();
342
343 // now signal the event
344 if (event->mask & EventAccess) {
345// qCDebug(BALOO) << path << "EventAccess";
346 Q_EMIT accessed(fname);
347 }
348 if (event->mask & EventAttributeChange) {
349// qCDebug(BALOO) << path << "EventAttributeChange";
351 }
352 if (event->mask & EventCloseWrite) {
353// qCDebug(BALOO) << path << "EventCloseWrite";
354 Q_EMIT closedWrite(fname);
355 }
356 if (event->mask & EventCloseRead) {
357// qCDebug(BALOO) << path << "EventCloseRead";
358 Q_EMIT closedRead(fname);
359 }
360 if (event->mask & EventCreate) {
361// qCDebug(BALOO) << path << "EventCreate";
362 Q_EMIT created(fname, 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(fname);
367 }
368 }
369 if (event->mask & EventDeleteSelf) {
370// qCDebug(BALOO) << path << "EventDeleteSelf";
371 d->removeWatch(event->wd);
372 Q_EMIT deleted(fname, 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(fname, false);
379 }
380 }
381 if (event->mask & EventModify) {
382// qCDebug(BALOO) << path << "EventModify";
383 Q_EMIT modified(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, path, WatchFlags(event->mask) };
398 }
399 if (event->mask & EventMoveTo) {
400 // check if we have a cookie for this one
401 if (d->cookies.contains(event->cookie)) {
402 const QByteArray oldPath = d->cookies.take(event->cookie).path;
403
404 // update the path cache
405 if (event->mask & IN_ISDIR) {
406 auto it = d->pathWatchHash.find(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(path, wd);
413 }
414 }
415// qCDebug(BALOO) << oldPath << "EventMoveTo" << path;
416 Q_EMIT moved(QFile::decodeName(oldPath), fname);
417 } else {
418// qCDebug(BALOO) << "No cookie for move information of" << path << "simulating new file event";
419 Q_EMIT created(fname, event->mask & IN_ISDIR);
420 if (event->mask & IN_ISDIR) {
421 handleDirCreated(fname);
422 }
423 }
424 }
425 if (event->mask & EventOpen) {
426// qCDebug(BALOO) << path << "EventOpen";
427 Q_EMIT opened(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(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(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(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((*it).path);
470 removeWatch(fname);
471 Q_EMIT deleted(fname, (*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"
Active config class which emits signals if the config was changed, for example if the KCM saved the c...
A simple wrapper around inotify which only allows to add folders recursively.
Definition kinotify.h:25
void installedWatches()
This is emitted once watches have been installed in all the directories indicated by addWatch.
void created(const QString &file, bool isDir)
Emitted if a new file has been created in one of the watched folders (KInotify::EventCreate)
@ EventUnmount
Backing fs was unmounted (compare inotify's IN_UNMOUNT)
Definition kinotify.h:51
@ EventMoveFrom
File moved out of watched directory (compare inotify's IN_MOVED_FROM)
Definition kinotify.h:48
@ EventOpen
File was opened (compare inotify's IN_OPEN)
Definition kinotify.h:50
@ EventMoveSelf
Watched file/directory was itself moved (compare inotify's IN_MOVE_SELF)
Definition kinotify.h:47
@ EventDelete
File/directory created in watched directory (compare inotify's IN_CREATE)
Definition kinotify.h:44
@ EventDeleteSelf
Watched file/directory was itself deleted (compare inotify's IN_DELETE_SELF)
Definition kinotify.h:45
@ EventMoveTo
File moved into watched directory (compare inotify's IN_MOVED_TO)
Definition kinotify.h:49
@ EventCloseRead
File not opened for writing was closed (compare inotify's IN_CLOSE_NOWRITE)
Definition kinotify.h:42
@ EventCloseWrite
File opened for writing was closed (compare inotify's IN_CLOSE_WRITE)
Definition kinotify.h:41
@ EventQueueOverflow
Event queued overflowed (compare inotify's IN_Q_OVERFLOW)
Definition kinotify.h:52
@ EventModify
File was modified (compare inotify's IN_MODIFY)
Definition kinotify.h:46
@ EventAccess
File was accessed (read, compare inotify's IN_ACCESS)
Definition kinotify.h:39
@ EventIgnored
File was ignored (compare inotify's IN_IGNORED)
Definition kinotify.h:53
@ EventAttributeChange
Metadata changed (permissions, timestamps, extended attributes, etc., compare inotify's IN_ATTRIB)
Definition kinotify.h:40
void modified(const QString &file)
Emitted if a watched file is modified (KInotify::EventModify)
void moved(const QString &oldName, const QString &newName)
Emitted if a file or folder has been moved or renamed.
void deleted(const QString &file, bool isDir)
Emitted if a watched file or folder has been deleted.
void watchUserLimitReached(const QString &path)
Emitted if during updating the internal watch structures (recursive watches) the inotify user watch l...
void resetUserLimit()
Call this when the inotify limit has been increased.
Definition kinotify.cpp:223
void accessed(const QString &file)
Emitted if a file is accessed (KInotify::EventAccess)
void closedWrite(const QString &file)
Emitted if FIXME (KInotify::EventCloseWrite)
void closedRead(const QString &file)
Emitted if FIXME (KInotify::EventCloseRead)
void unmounted(const QString &file)
Emitted if a watched path has been unmounted (KInotify::EventUnmount)
@ FlagExclUnlink
Do not generate events for unlinked files (IN_EXCL_UNLINK)
Definition kinotify.h:79
void opened(const QString &file)
Emitted if a file is opened (KInotify::EventOpen)
void attributeChanged(const QString &file)
Emitted if file attributes are changed (KInotify::EventAttributeChange)
QVariant read(const QByteArray &data, int versionOverride=0)
QString path(const QString &relativePath)
char * data()
QByteArray fromRawData(const char *data, qsizetype size)
bool isEmpty() const const
QDeadlineTimer current(Qt::TimerType timerType)
QString decodeName(const QByteArray &localFileName)
QByteArray encodeName(const QString &fileName)
iterator begin()
bool contains(const Key &key) const const
qsizetype count() const const
bool empty() const const
iterator end()
iterator erase(const_iterator pos)
iterator find(const Key &key)
iterator insert(const Key &key, const T &value)
bool remove(const Key &key)
T take(const Key &key)
T value(const Key &key) const const
bool isEmpty() const const
value_type takeFirst()
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual bool event(QEvent *e)
QObject * parent() const const
void activated(QSocketDescriptor socket, QSocketNotifier::Type type)
QString & append(QChar ch)
QString chopped(qsizetype len) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QueuedConnection
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isActive() const const
void start()
void stop()
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:56:58 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.