Qt 4.8
qfilesystemwatcher_fsevents.cpp
Go to the documentation of this file.
1 /****************************************************************************
2 **
3 ** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/legal
5 **
6 ** This file is part of the QtCore module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and Digia. For licensing terms and
14 ** conditions see http://qt.digia.com/licensing. For further information
15 ** use the contact form at http://qt.digia.com/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** In addition, as a special exception, Digia gives you certain additional
26 ** rights. These rights are described in the Digia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28 **
29 ** GNU General Public License Usage
30 ** Alternatively, this file may be used under the terms of the GNU
31 ** General Public License version 3.0 as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL included in the
33 ** packaging of this file. Please review the following information to
34 ** ensure the GNU General Public License version 3.0 requirements will be
35 ** met: http://www.gnu.org/copyleft/gpl.html.
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41 
42 
43 #include <qplatformdefs.h>
44 
45 #include "qfilesystemwatcher.h"
47 
48 #ifndef QT_NO_FILESYSTEMWATCHER
49 
50 #include <qdebug.h>
51 #include <qfile.h>
52 #include <qdatetime.h>
53 #include <qfileinfo.h>
54 #include <qvarlengtharray.h>
55 
56 #include <mach/mach.h>
57 #include <sys/types.h>
58 #include <CoreFoundation/CFRunLoop.h>
59 #include <CoreFoundation/CFUUID.h>
60 #if !defined(Q_OS_IOS)
61 #include <CoreServices/CoreServices.h>
62 #endif
63 #include <AvailabilityMacros.h>
64 #include <private/qcore_mac_p.h>
65 
67 
68 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
69 // Static operator overloading so for the sake of some convieniece.
70 // They only live in this compilation unit to avoid polluting Qt in general.
71 static bool operator==(const struct ::timespec &left, const struct ::timespec &right)
72 {
73  return left.tv_sec == right.tv_sec
74  && left.tv_nsec == right.tv_nsec;
75 }
76 
77 static bool operator==(const struct ::stat64 &left, const struct ::stat64 &right)
78 {
79  return left.st_dev == right.st_dev
80  && left.st_mode == right.st_mode
81  && left.st_size == right.st_size
82  && left.st_ino == right.st_ino
83  && left.st_uid == right.st_uid
84  && left.st_gid == right.st_gid
85  && left.st_mtimespec == right.st_mtimespec
86  && left.st_ctimespec == right.st_ctimespec
87  && left.st_flags == right.st_flags;
88 }
89 
90 static bool operator!=(const struct ::stat64 &left, const struct ::stat64 &right)
91 {
92  return !(operator==(left, right));
93 }
94 
95 
96 static void addPathToHash(PathHash &pathHash, const QString &key, const QFileInfo &fileInfo,
97  const QString &path)
98 {
99  PathInfoList &list = pathHash[key];
100  list.push_back(PathInfo(path,
102  pathHash.insert(key, list);
103 }
104 
105 static void removePathFromHash(PathHash &pathHash, const QString &key, const QString &path)
106 {
107  PathInfoList &list = pathHash[key];
108  // We make the assumption that the list contains unique paths
109  PathInfoList::iterator End = list.end();
110  PathInfoList::iterator it = list.begin();
111  while (it != End) {
112  if (it->originalPath == path) {
113  list.erase(it);
114  break;
115  }
116  ++it;
117  }
118  if (list.isEmpty())
119  pathHash.remove(key);
120 }
121 
122 static void stopFSStream(FSEventStreamRef stream)
123 {
124  if (stream) {
125  FSEventStreamStop(stream);
126  FSEventStreamInvalidate(stream);
127  }
128 }
129 
130 static QString createFSStreamPath(const QString &absolutePath)
131 {
132  // The path returned has a trailing slash, so ensure that here.
133  QString string = absolutePath;
134  string.reserve(string.size() + 1);
135  string.append(QLatin1Char('/'));
136  return string;
137 }
138 
139 static void cleanupFSStream(FSEventStreamRef stream)
140 {
141  if (stream)
142  FSEventStreamRelease(stream);
143 }
144 
145 const FSEventStreamCreateFlags QtFSEventFlags = (kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagNoDefer /* | kFSEventStreamCreateFlagWatchRoot*/);
146 
147 const CFTimeInterval Latency = 0.033; // This will do updates 30 times a second which is probably more than you need.
148 #endif
149 
151  : fsStream(0), pathsToWatch(0), threadsRunLoop(0)
152 {
153 }
154 
156 {
157 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
158  // I assume that at this point, QFileSystemWatcher has already called stop
159  // on me, so I don't need to invalidate or stop my stream, simply
160  // release it.
161  cleanupFSStream(fsStream);
162  if (pathsToWatch)
163  CFRelease(pathsToWatch);
164 #endif
165 }
166 
168 {
170 }
171 
173  QStringList *files,
174  QStringList *directories)
175 {
176 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
177  stop();
178  wait();
179  QMutexLocker locker(&mutex);
180  QStringList failedToAdd;
181  // if we have a running FSStreamEvent, we have to kill it, we'll re-add the stream soon.
182  FSEventStreamEventId idToCheck;
183  if (fsStream) {
184  idToCheck = FSEventStreamGetLatestEventId(fsStream);
185  cleanupFSStream(fsStream);
186  } else {
187  idToCheck = kFSEventStreamEventIdSinceNow;
188  }
189 
190  // Brain-dead approach, but works. FSEvents actually can already read sub-trees, but since it's
191  // work to figure out if we are doing a double register, we just register it twice as FSEvents
192  // seems smart enough to only deliver one event. We also duplicate directory entries in here
193  // (e.g., if you watch five files in the same directory, you get that directory included in the
194  // array 5 times). This stupidity also makes remove work correctly though. I'll freely admit
195  // that we could make this a bit smarter. If you do, check the auto-tests, they should catch at
196  // least a couple of the issues.
197  QCFType<CFMutableArrayRef> tmpArray = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
198  for (int i = 0; i < paths.size(); ++i) {
199  const QString &path = paths.at(i);
200 
201  QFileInfo fileInfo(path);
202  if (!fileInfo.exists()) {
203  failedToAdd.append(path);
204  continue;
205  }
206 
207  if (fileInfo.isDir()) {
208  if (directories->contains(path)) {
209  failedToAdd.append(path);
210  continue;
211  } else {
212  directories->append(path);
213  // Full file path for dirs.
214  QCFString cfpath(createFSStreamPath(fileInfo.canonicalFilePath()));
215  addPathToHash(dirPathInfoHash, cfpath, fileInfo, path);
216  CFArrayAppendValue(tmpArray, cfpath);
217  }
218  } else {
219  if (files->contains(path)) {
220  failedToAdd.append(path);
221  continue;
222  } else {
223  // Just the absolute path (minus it's filename) for files.
224  QCFString cfpath(createFSStreamPath(fileInfo.canonicalPath()));
225  files->append(path);
226  addPathToHash(filePathInfoHash, cfpath, fileInfo, path);
227  CFArrayAppendValue(tmpArray, cfpath);
228  }
229  }
230  }
231 
232  if (!pathsToWatch && failedToAdd.size() == paths.size()) {
233  return failedToAdd;
234  }
235 
236  if (CFArrayGetCount(tmpArray) > 0) {
237  if (pathsToWatch) {
238  CFArrayAppendArray(tmpArray, pathsToWatch, CFRangeMake(0, CFArrayGetCount(pathsToWatch)));
239  CFRelease(pathsToWatch);
240  }
241  pathsToWatch = CFArrayCreateCopy(kCFAllocatorDefault, tmpArray);
242  }
243 
244  FSEventStreamContext context = { 0, this, 0, 0, 0 };
245  fsStream = FSEventStreamCreate(kCFAllocatorDefault,
247  &context, pathsToWatch,
248  idToCheck, Latency, QtFSEventFlags);
249  warmUpFSEvents();
250 
251  return failedToAdd;
252 #else
253  Q_UNUSED(paths);
254  Q_UNUSED(files);
255  Q_UNUSED(directories);
256  return QStringList();
257 #endif
258 }
259 
261 {
262 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
263  // This function assumes that the mutex has already been grabbed before calling it.
264  // It exits with the mutex still locked (Q_ASSERT(mutex.isLocked()) ;-).
265  start();
267 #endif
268 }
269 
271  QStringList *files,
272  QStringList *directories)
273 {
274 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
275  stop();
276  wait();
277  QMutexLocker locker(&mutex);
278  // short circuit for smarties that call remove before add and we have nothing.
279  if (pathsToWatch == 0)
280  return paths;
281  QStringList failedToRemove;
282  // if we have a running FSStreamEvent, we have to stop it, we'll re-add the stream soon.
283  FSEventStreamEventId idToCheck;
284  if (fsStream) {
285  idToCheck = FSEventStreamGetLatestEventId(fsStream);
286  cleanupFSStream(fsStream);
287  fsStream = 0;
288  } else {
289  idToCheck = kFSEventStreamEventIdSinceNow;
290  }
291 
292  CFIndex itemCount = CFArrayGetCount(pathsToWatch);
293  QCFType<CFMutableArrayRef> tmpArray = CFArrayCreateMutableCopy(kCFAllocatorDefault, itemCount,
294  pathsToWatch);
295  CFRelease(pathsToWatch);
296  pathsToWatch = 0;
297  for (int i = 0; i < paths.size(); ++i) {
298  // Get the itemCount at the beginning to avoid any overruns during the iteration.
299  itemCount = CFArrayGetCount(tmpArray);
300  const QString &path = paths.at(i);
301  QFileInfo fi(path);
302  QCFString cfpath(createFSStreamPath(fi.canonicalPath()));
303 
304  CFIndex index = CFArrayGetFirstIndexOfValue(tmpArray, CFRangeMake(0, itemCount), cfpath);
305  if (index != -1) {
306  CFArrayRemoveValueAtIndex(tmpArray, index);
307  files->removeAll(path);
308  removePathFromHash(filePathInfoHash, cfpath, path);
309  } else {
310  // Could be a directory we are watching instead.
311  QCFString cfdirpath(createFSStreamPath(fi.canonicalFilePath()));
312  index = CFArrayGetFirstIndexOfValue(tmpArray, CFRangeMake(0, itemCount), cfdirpath);
313  if (index != -1) {
314  CFArrayRemoveValueAtIndex(tmpArray, index);
315  directories->removeAll(path);
316  removePathFromHash(dirPathInfoHash, cfpath, path);
317  } else {
318  failedToRemove.append(path);
319  }
320  }
321  }
322  itemCount = CFArrayGetCount(tmpArray);
323  if (itemCount != 0) {
324  pathsToWatch = CFArrayCreateCopy(kCFAllocatorDefault, tmpArray);
325 
326  FSEventStreamContext context = { 0, this, 0, 0, 0 };
327  fsStream = FSEventStreamCreate(kCFAllocatorDefault,
329  &context, pathsToWatch, idToCheck, Latency, QtFSEventFlags);
330  warmUpFSEvents();
331  }
332  return failedToRemove;
333 #else
334  Q_UNUSED(paths);
335  Q_UNUSED(files);
336  Q_UNUSED(directories);
337  return QStringList();
338 #endif
339 }
340 
341 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
342 void QFSEventsFileSystemWatcherEngine::updateList(PathInfoList &list, bool directory, bool emitSignals)
343 {
344  PathInfoList::iterator End = list.end();
345  PathInfoList::iterator it = list.begin();
346  while (it != End) {
347  struct ::stat64 newInfo;
348  if (::stat64(it->absolutePath, &newInfo) == 0) {
349  if (emitSignals) {
350  if (newInfo != it->savedInfo) {
351  it->savedInfo = newInfo;
352  if (directory)
353  emit directoryChanged(it->originalPath, false);
354  else
355  emit fileChanged(it->originalPath, false);
356  }
357  } else {
358  it->savedInfo = newInfo;
359  }
360  } else {
361  if (errno == ENOENT) {
362  if (emitSignals) {
363  if (directory)
364  emit directoryChanged(it->originalPath, true);
365  else
366  emit fileChanged(it->originalPath, true);
367  }
368  it = list.erase(it);
369  continue;
370  } else {
371  qWarning("%s:%d:QFSEventsFileSystemWatcherEngine: stat error on %s:%s",
372  __FILE__, __LINE__, qPrintable(it->originalPath), strerror(errno));
373 
374  }
375  }
376  ++it;
377  }
378 }
379 
380 void QFSEventsFileSystemWatcherEngine::updateHash(PathHash &pathHash)
381 {
382  PathHash::iterator HashEnd = pathHash.end();
383  PathHash::iterator it = pathHash.begin();
384  const bool IsDirectory = (&pathHash == &dirPathInfoHash);
385  while (it != HashEnd) {
386  updateList(it.value(), IsDirectory, false);
387  if (it.value().isEmpty())
388  it = pathHash.erase(it);
389  else
390  ++it;
391  }
392 }
393 #endif
394 
396  void *clientCallBackInfo, size_t numEvents,
397  void *eventPaths,
398  const FSEventStreamEventFlags eventFlags[],
399  const FSEventStreamEventId [])
400 {
401 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
402  QFSEventsFileSystemWatcherEngine *watcher = static_cast<QFSEventsFileSystemWatcherEngine *>(clientCallBackInfo);
403  QMutexLocker locker(&watcher->mutex);
404  CFArrayRef paths = static_cast<CFArrayRef>(eventPaths);
405  for (size_t i = 0; i < numEvents; ++i) {
406  const QString path = QCFString::toQString(
407  static_cast<CFStringRef>(CFArrayGetValueAtIndex(paths, i)));
408  const FSEventStreamEventFlags pathFlags = eventFlags[i];
409  // There are several flags that may be passed, but we really don't care about them ATM.
410  // Here they are and why we don't care.
411  // kFSEventStreamEventFlagHistoryDone--(very unlikely to be gotten, but even then, not much changes).
412  // kFSEventStreamEventFlagMustScanSubDirs--Likely means the data is very much out of date, we
413  // aren't coalescing our directories, so again not so much of an issue
414  // kFSEventStreamEventFlagRootChanged | kFSEventStreamEventFlagMount | kFSEventStreamEventFlagUnmount--
415  // These three flags indicate something has changed, but the stat will likely show this, so
416  // there's not really much to worry about.
417  // (btw, FSEvents is not the correct way of checking for mounts/unmounts,
418  // there are real CarbonCore events for that.)
419  Q_UNUSED(pathFlags);
420  if (watcher->filePathInfoHash.contains(path))
421  watcher->updateList(watcher->filePathInfoHash[path], false, true);
422 
423  if (watcher->dirPathInfoHash.contains(path))
424  watcher->updateList(watcher->dirPathInfoHash[path], true, true);
425  }
426 #else
427  Q_UNUSED(clientCallBackInfo);
428  Q_UNUSED(numEvents);
429  Q_UNUSED(eventPaths);
430  Q_UNUSED(eventFlags);
431 #endif
432 }
433 
435 {
436 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
437  QMutexLocker locker(&mutex);
438  stopFSStream(fsStream);
439  if (threadsRunLoop) {
440  CFRunLoopStop(threadsRunLoop);
442  }
443 #endif
444 }
445 
447 {
448 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
449  QMutexLocker locker(&mutex);
450  updateHash(filePathInfoHash);
451  updateHash(dirPathInfoHash);
452  if (filePathInfoHash.isEmpty() && dirPathInfoHash.isEmpty()) {
453  // Everything disappeared before we got to start, don't bother.
454 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
455  // Code duplicated from stop(), with the exception that we
456  // don't wait on waitForStop here. Doing this will lead to
457  // a deadlock since this function is called from the worker
458  // thread. (waitForStop.wakeAll() is only called from the
459  // end of run()).
460  stopFSStream(fsStream);
461  if (threadsRunLoop)
462  CFRunLoopStop(threadsRunLoop);
463 #endif
464  cleanupFSStream(fsStream);
465  }
467 #endif
468 }
469 
471 {
472 #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 && !defined(Q_OS_IOS)
473  threadsRunLoop = CFRunLoopGetCurrent();
474  FSEventStreamScheduleWithRunLoop(fsStream, threadsRunLoop, kCFRunLoopDefaultMode);
475  bool startedOK = FSEventStreamStart(fsStream);
476  // It's recommended by Apple that you only update the files after you've started
477  // the stream, because otherwise you might miss an update in between starting it.
478  updateFiles();
479 #ifdef QT_NO_DEBUG
480  Q_UNUSED(startedOK);
481 #else
482  Q_ASSERT(startedOK);
483 #endif
484  // If for some reason we called stop up above (and invalidated our stream), this call will return
485  // immediately.
486  CFRunLoopRun();
487  threadsRunLoop = 0;
488  QMutexLocker locker(&mutex);
490 #endif
491 }
492 
494 #endif //QT_NO_FILESYSTEMWATCHER
void fileChanged(const QString &path, bool removed)
#define QT_END_NAMESPACE
This macro expands to.
Definition: qglobal.h:90
int remove(const Key &key)
Removes all the items that have the key from the hash.
Definition: qhash.h:784
QStringList addPaths(const QStringList &paths, QStringList *files, QStringList *directories)
#define it(className, varName)
QByteArray toUtf8() const Q_REQUIRED_RESULT
Returns a UTF-8 representation of the string as a QByteArray.
Definition: qstring.cpp:4074
static QFSEventsFileSystemWatcherEngine * create()
const struct __FSEventStream * ConstFSEventStreamRef
static QString toQString(CFStringRef cfstr)
Definition: qcore_mac.cpp:47
bool exists() const
Returns true if the file exists; otherwise returns false.
Definition: qfileinfo.cpp:675
UInt32 FSEventStreamEventFlags
The QString class provides a Unicode character string.
Definition: qstring.h:83
QString normalized(NormalizationForm mode) const Q_REQUIRED_RESULT
Returns the string in the given Unicode normalization mode.
Definition: qstring.cpp:6635
#define Q_ASSERT(cond)
Definition: qglobal.h:1823
struct __FSEventStream * FSEventStreamRef
iterator insert(const Key &key, const T &value)
Inserts a new item with the key and a value of value.
Definition: qhash.h:753
void reserve(int size)
Attempts to allocate memory for at least size characters.
Definition: qstring.h:881
void append(const T &t)
Inserts value at the end of the list.
Definition: qlist.h:507
QTextStream & right(QTextStream &stream)
Calls QTextStream::setFieldAlignment(QTextStream::AlignRight) on stream and returns stream...
#define QT_BEGIN_NAMESPACE
This macro expands to.
Definition: qglobal.h:89
static FILE * stream
bool isDir() const
Returns true if this object points to a directory or to a symbolic link to a directory; otherwise ret...
Definition: qfileinfo.cpp:990
const T & at(int i) const
Returns the item at index position i in the list.
Definition: qlist.h:468
#define emit
Definition: qobjectdefs.h:76
The QStringList class provides a list of strings.
Definition: qstringlist.h:66
QString canonicalFilePath() const
Returns the canonical path including the file name, i.e.
Definition: qfileinfo.cpp:551
Q_CORE_EXPORT void qWarning(const char *,...)
QBool contains(const QString &str, Qt::CaseSensitivity cs=Qt::CaseSensitive) const
Returns true if the list contains the string str; otherwise returns false.
Definition: qstringlist.h:172
static void fseventsCallback(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[])
void directoryChanged(const QString &path, bool removed)
QString canonicalPath() const
Returns the file&#39;s path canonical path (excluding the file name), i.e.
Definition: qfileinfo.cpp:598
bool wait(QMutex *mutex, unsigned long time=ULONG_MAX)
bool operator!=(const T *o, const QPointer< T > &p)
Definition: qpointer.h:122
The QMutexLocker class is a convenience class that simplifies locking and unlocking mutexes...
Definition: qmutex.h:101
iterator end()
Returns an STL-style iterator pointing to the imaginary item after the last item in the hash...
Definition: qhash.h:467
void start(Priority=InheritPriority)
Begins execution of the thread by calling run().
bool wait(unsigned long time=ULONG_MAX)
Blocks the thread until either of these conditions is met:
int key
int size() const
Returns the number of items in the list.
Definition: qlist.h:137
iterator begin()
Returns an STL-style iterator pointing to the first item in the hash.
Definition: qhash.h:464
bool operator==(const T *o, const QPointer< T > &p)
Definition: qpointer.h:87
quint16 index
uint64_t FSEventStreamEventId
The QFileInfo class provides system-independent file information.
Definition: qfileinfo.h:60
const struct __CFArray * CFArrayRef
#define qPrintable(string)
Definition: qglobal.h:1750
#define Q_UNUSED(x)
Indicates to the compiler that the parameter with the specified name is not used in the body of a fun...
Definition: qglobal.h:1729
QStringList removePaths(const QStringList &paths, QStringList *files, QStringList *directories)
virtual IFMETHOD End(void)=0
iterator erase(iterator it)
Removes the (key, value) pair associated with the iterator pos from the hash, and returns an iterator...
Definition: qhash.h:827
The QLatin1Char class provides an 8-bit ASCII/Latin-1 character.
Definition: qchar.h:55
friend class iterator
Definition: qhash.h:393
int errno
void run()
The starting point for the thread.
int removeAll(const T &t)
Removes all occurrences of value in the list and returns the number of entries removed.
Definition: qlist.h:770
QTextStream & left(QTextStream &stream)
Calls QTextStream::setFieldAlignment(QTextStream::AlignLeft) on stream and returns stream...