Baloo

kinotify.cpp
1 /*
2  This file is part of the KDE libraries
3  SPDX-FileCopyrightText: 2007-2010 Sebastian Trueg <[email protected]>
4  SPDX-FileCopyrightText: 2012-2014 Vishesh Handa <[email protected]>
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 
31 namespace
32 {
33 QByteArray normalizeTrailingSlash(QByteArray&& path)
34 {
35  if (!path.endsWith('/')) {
36  path.append('/');
37  }
38  return path;
39 }
40 
41 QByteArray 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 
52 class KInotify::Private
53 {
54 public:
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;
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;
136  Q_EMIT q->watchUserLimitReached(path);
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 {
177  Q_EMIT q->installedWatches();
178  }
179  }
180 
181 private:
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 
200 KInotify::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 
212 KInotify::~KInotify()
213 {
214  delete d;
215 }
216 
217 bool 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 
228 bool 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 
247 bool 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 
277 void 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 
292 void 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";
350  Q_EMIT attributeChanged(fname);
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 
462 void 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"
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QByteArray fromRawData(const char *data, int size)
Q_EMITQ_EMIT
QAction * open(const QObject *recvr, const char *slot, QObject *parent)
QByteArray encodeName(const QString &fileName)
QString chopped(int len) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QHash::iterator insert(const Key &key, const T &value)
T take(const Key &key)
T takeFirst()
void timeout()
bool isEmpty() const const
Active config class which emits signals if the config was changed, for example if the KCM saved the c...
QAction * close(const QObject *recvr, const char *slot, QObject *parent)
@ EventUnmount
Backing fs was unmounted (compare inotify's IN_UNMOUNT)
Definition: kinotify.h:51
QueuedConnection
bool isEmpty() const const
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
KSharedConfigPtr config()
A simple wrapper around inotify which only allows to add folders recursively.
Definition: kinotify.h:24
QDeadlineTimer current(Qt::TimerType timerType)
bool isEmpty() const const
@ FlagExclUnlink
Do not generate events for unlinked files (IN_EXCL_UNLINK)
Definition: kinotify.h:79
int remove(const Key &key)
QString path(const QString &relativePath)
QString & insert(int position, QChar ch)
bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9)
QVariant read(const QByteArray &data, int versionOverride=0)
int count(const Key &key) const const
bool contains(const Key &key) const const
void resetUserLimit()
Call this when the inotify limit has been increased.
Definition: kinotify.cpp:223
QObject * parent() const const
void activated(QSocketDescriptor socket, QSocketNotifier::Type type)
QString & append(QChar ch)
char * data()
QString decodeName(const QByteArray &localFileName)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Wed Nov 29 2023 03:56:26 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.