Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
qfilesystemwatcher_fsevents.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <qplatformdefs.h>
5
6#include "qdiriterator.h"
9#include "private/qcore_unix_p.h"
10#include "kernel/qcore_mac_p.h"
11
12#include <qdebug.h>
13#include <qdir.h>
14#include <qfile.h>
15#include <qfileinfo.h>
16#include <qvarlengtharray.h>
17#include <qscopeguard.h>
18
19#undef FSEVENT_DEBUG
20#ifdef FSEVENT_DEBUG
21# define DEBUG if (true) qDebug
22#else
23# define DEBUG if (false) qDebug
24#endif
25
27
28static void callBackFunction(ConstFSEventStreamRef streamRef,
29 void *clientCallBackInfo,
30 size_t numEvents,
31 void *eventPaths,
32 const FSEventStreamEventFlags eventFlags[],
33 const FSEventStreamEventId eventIds[])
34{
36
37 char **paths = static_cast<char **>(eventPaths);
39 engine->processEvent(streamRef, numEvents, paths, eventFlags, eventIds);
40}
41
42bool QFseventsFileSystemWatcherEngine::checkDir(DirsByName::iterator &it)
43{
44 bool needsRestart = false;
45
46 QT_STATBUF st;
47 const QString &name = it.key();
48 Info &info = it->dirInfo;
49 const int res = QT_STAT(QFile::encodeName(name), &st);
50 if (res == -1) {
51 needsRestart |= derefPath(info.watchedPath);
52 emit emitDirectoryChanged(info.origPath, true);
53 it = watchingState.watchedDirectories.erase(it);
54 } else if (st.st_ctimespec != info.ctime || st.st_mode != info.mode) {
55 info.ctime = st.st_ctimespec;
56 info.mode = st.st_mode;
57 emit emitDirectoryChanged(info.origPath, false);
58 ++it;
59 } else {
60 bool dirChanged = false;
61 InfoByName &entries = it->entries;
62 // check known entries:
63 for (InfoByName::iterator i = entries.begin(); i != entries.end(); ) {
64 if (QT_STAT(QFile::encodeName(i.key()), &st) == -1) {
65 // entry disappeared
66 dirChanged = true;
67 i = entries.erase(i);
68 } else {
69 if (i->ctime != st.st_ctimespec || i->mode != st.st_mode) {
70 // entry changed
71 dirChanged = true;
72 i->ctime = st.st_ctimespec;
73 i->mode = st.st_mode;
74 }
75 ++i;
76 }
77 }
78 // check for new entries:
79 QDirIterator dirIt(name);
80 while (dirIt.hasNext()) {
81 dirIt.next();
82 QString entryName = dirIt.filePath();
83 if (!entries.contains(entryName)) {
84 dirChanged = true;
85 QT_STATBUF st;
86 if (QT_STAT(QFile::encodeName(entryName), &st) == -1)
87 continue;
88 entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString()));
89
90 }
91 }
92 if (dirChanged)
93 emit emitDirectoryChanged(info.origPath, false);
94 ++it;
95 }
96
97 return needsRestart;
98}
99
100bool QFseventsFileSystemWatcherEngine::rescanDirs(const QString &path)
101{
102 bool needsRestart = false;
103
104 for (DirsByName::iterator it = watchingState.watchedDirectories.begin();
105 it != watchingState.watchedDirectories.end(); ) {
106 if (it.key().startsWith(path))
107 needsRestart |= checkDir(it);
108 else
109 ++it;
110 }
111
112 return needsRestart;
113}
114
115bool QFseventsFileSystemWatcherEngine::rescanFiles(InfoByName &filesInPath)
116{
117 bool needsRestart = false;
118
119 for (InfoByName::iterator it = filesInPath.begin(); it != filesInPath.end(); ) {
120 QT_STATBUF st;
121 QString name = it.key();
122 const int res = QT_STAT(QFile::encodeName(name), &st);
123 if (res == -1) {
124 needsRestart |= derefPath(it->watchedPath);
125 emit emitFileChanged(it.value().origPath, true);
126 it = filesInPath.erase(it);
127 continue;
128 } else if (st.st_ctimespec != it->ctime || st.st_mode != it->mode) {
129 it->ctime = st.st_ctimespec;
130 it->mode = st.st_mode;
131 emit emitFileChanged(it.value().origPath, false);
132 }
133
134 ++it;
135 }
136
137 return needsRestart;
138}
139
140bool QFseventsFileSystemWatcherEngine::rescanFiles(const QString &path)
141{
142 bool needsRestart = false;
143
144 for (FilesByPath::iterator i = watchingState.watchedFiles.begin();
145 i != watchingState.watchedFiles.end(); ) {
146 if (i.key().startsWith(path)) {
147 needsRestart |= rescanFiles(i.value());
148 if (i.value().isEmpty()) {
149 i = watchingState.watchedFiles.erase(i);
150 continue;
151 }
152 }
153
154 ++i;
155 }
156
157 return needsRestart;
158}
159
160void QFseventsFileSystemWatcherEngine::processEvent(ConstFSEventStreamRef streamRef,
161 size_t numEvents,
162 char **eventPaths,
163 const FSEventStreamEventFlags eventFlags[],
164 const FSEventStreamEventId eventIds[])
165{
166#if defined(Q_OS_MACOS)
167 Q_UNUSED(streamRef);
168
169 bool needsRestart = false;
170
171 QMutexLocker locker(&lock);
172
173 for (size_t i = 0; i < numEvents; ++i) {
174 FSEventStreamEventFlags eFlags = eventFlags[i];
175 DEBUG("Change %llu in %s, flags %x", eventIds[i], eventPaths[i], (unsigned int)eFlags);
176
177 if (eFlags & kFSEventStreamEventFlagEventIdsWrapped) {
178 DEBUG("\tthe event ids wrapped");
179 lastReceivedEvent = 0;
180 }
181 lastReceivedEvent = qMax(lastReceivedEvent, eventIds[i]);
182
183 QString path = QFile::decodeName(eventPaths[i]);
184 if (path.size() > 1 && path.endsWith(QDir::separator()))
185 path.chop(1);
186
187 if (eFlags & kFSEventStreamEventFlagMustScanSubDirs) {
188 DEBUG("\tmust rescan directory because of coalesced events");
189 if (eFlags & kFSEventStreamEventFlagUserDropped)
190 DEBUG("\t\t... user dropped.");
191 if (eFlags & kFSEventStreamEventFlagKernelDropped)
192 DEBUG("\t\t... kernel dropped.");
193 needsRestart |= rescanDirs(path);
194 needsRestart |= rescanFiles(path);
195 continue;
196 }
197
198 if (eFlags & kFSEventStreamEventFlagRootChanged) {
199 // re-check everything:
200 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(path);
201 if (dirIt != watchingState.watchedDirectories.end())
202 needsRestart |= checkDir(dirIt);
203 needsRestart |= rescanFiles(path);
204 continue;
205 }
206
207 if ((eFlags & kFSEventStreamEventFlagItemIsDir) && (eFlags & kFSEventStreamEventFlagItemRemoved))
208 needsRestart |= rescanDirs(path);
209
210 // check watched directories:
211 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(path);
212 if (dirIt != watchingState.watchedDirectories.end())
213 needsRestart |= checkDir(dirIt);
214
215 // check watched files:
216 FilesByPath::iterator pIt = watchingState.watchedFiles.find(path);
217 if (pIt != watchingState.watchedFiles.end())
218 needsRestart |= rescanFiles(pIt.value());
219 }
220
221 if (needsRestart)
223#else
224 Q_UNUSED(streamRef);
225 Q_UNUSED(numEvents);
226 Q_UNUSED(eventPaths);
227 Q_UNUSED(eventFlags);
228 Q_UNUSED(eventIds);
229#endif
230}
231
232void QFseventsFileSystemWatcherEngine::doEmitFileChanged(const QString &path, bool removed)
233{
234 DEBUG() << "emitting fileChanged for" << path << "with removed =" << removed;
235 emit fileChanged(path, removed);
236}
237
238void QFseventsFileSystemWatcherEngine::doEmitDirectoryChanged(const QString &path, bool removed)
239{
240 DEBUG() << "emitting directoryChanged for" << path << "with removed =" << removed;
241 emit directoryChanged(path, removed);
242}
243
244bool QFseventsFileSystemWatcherEngine::restartStream()
245{
246 QMutexLocker locker(&lock);
247 stopStream();
248 return startStream();
249}
250
255
256QFseventsFileSystemWatcherEngine::QFseventsFileSystemWatcherEngine(QObject *parent)
258 , stream(0)
259 , lastReceivedEvent(kFSEventStreamEventIdSinceNow)
260{
261
262 // We cannot use signal-to-signal queued connections, because the
263 // QSignalSpy cannot spot signals fired from other/alien threads.
265 this, SLOT(doEmitDirectoryChanged(QString,bool)), Qt::QueuedConnection);
267 this, SLOT(doEmitFileChanged(QString,bool)), Qt::QueuedConnection);
269 this, SLOT(restartStream()), Qt::QueuedConnection);
270
271 queue = dispatch_queue_create("org.qt-project.QFseventsFileSystemWatcherEngine", NULL);
272}
273
275{
277
278 dispatch_sync(queue, ^{
279 // Stop the stream in case we have to wait for the lock below to be acquired.
280 if (stream)
281 FSEventStreamStop(stream);
282
283 // The assumption with the locking strategy is that this class cannot and will not be subclassed!
284 QMutexLocker locker(&lock);
285
286 stopStream(true);
287 });
288 dispatch_release(queue);
289}
290
293 QStringList *directories)
294{
296
297 if (stream) {
298 DEBUG("Flushing, last id is %llu", FSEventStreamGetLatestEventId(stream));
299 FSEventStreamFlushSync(stream);
300 }
301
302 QMutexLocker locker(&lock);
303
304 bool wasRunning = stream != nullptr;
305 bool needsRestart = false;
306
307 WatchingState oldState = watchingState;
308 QStringList unhandled;
309 for (const QString &path : paths) {
310 auto sg = qScopeGuard([&]{ unhandled.push_back(path); });
312 QString realPath = origPath;
313 if (realPath.size() > 1 && realPath.endsWith(QDir::separator()))
314 realPath.chop(1);
315 QString watchedPath, parentPath;
316
317 realPath = QFileInfo(realPath).canonicalFilePath();
318 QFileInfo fi(realPath);
319 if (realPath.isEmpty())
320 continue;
321
322 QT_STATBUF st;
323 if (QT_STAT(QFile::encodeName(realPath), &st) == -1)
324 continue;
325
326 const bool isDir = S_ISDIR(st.st_mode);
327 if (isDir) {
328 if (watchingState.watchedDirectories.contains(realPath))
329 continue;
330 directories->append(origPath);
331 watchedPath = realPath;
332 } else {
333 if (files->contains(origPath))
334 continue;
335 files->append(origPath);
336
337 watchedPath = fi.path();
338 parentPath = watchedPath;
339 }
340
341 sg.dismiss();
342
343 for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(),
344 ei = watchingState.watchedPaths.end(); i != ei; ++i) {
345 if (watchedPath.startsWith(i.key() % QDir::separator())) {
346 watchedPath = i.key();
347 break;
348 }
349 }
350
351 PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath);
352 if (it == watchingState.watchedPaths.end()) {
353 needsRestart = true;
354 watchingState.watchedPaths.insert(watchedPath, 1);
355 DEBUG("Adding '%s' to watchedPaths", qPrintable(watchedPath));
356 } else {
357 ++it.value();
358 }
359
360 Info info(origPath, st.st_ctimespec, st.st_mode, watchedPath);
361 if (isDir) {
362 DirInfo dirInfo;
363 dirInfo.dirInfo = info;
364 dirInfo.entries = scanForDirEntries(realPath);
365 watchingState.watchedDirectories.insert(realPath, dirInfo);
366 DEBUG("-- Also adding '%s' to watchedDirectories", qPrintable(realPath));
367 } else {
368 watchingState.watchedFiles[parentPath].insert(realPath, info);
369 DEBUG("-- Also adding '%s' to watchedFiles", qPrintable(realPath));
370 }
371 }
372
373 if (needsRestart) {
374 stopStream();
375 if (!startStream()) {
376 // ok, something went wrong, let's try to restore the previous state
377 watchingState = std::move(oldState);
378 // and because we don't know which path caused the issue (if any), fail on all of them
379 unhandled = paths;
380
381 if (wasRunning)
382 startStream();
383 }
384 }
385
386 return unhandled;
387}
388
391 QStringList *directories)
392{
394
395 QMutexLocker locker(&lock);
396
397 bool needsRestart = false;
398
399 WatchingState oldState = watchingState;
400 QStringList unhandled;
401 for (const QString &origPath : paths) {
402 auto sg = qScopeGuard([&]{ unhandled.push_back(origPath); });
403 QString realPath = origPath;
404 if (realPath.size() > 1 && realPath.endsWith(QDir::separator()))
405 realPath.chop(1);
406
407 QFileInfo fi(realPath);
408 realPath = fi.canonicalFilePath();
409
410 if (fi.isDir()) {
411 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(realPath);
412 if (dirIt != watchingState.watchedDirectories.end()) {
413 needsRestart |= derefPath(dirIt->dirInfo.watchedPath);
414 watchingState.watchedDirectories.erase(dirIt);
415 directories->removeAll(origPath);
416 sg.dismiss();
417 DEBUG("Removed directory '%s'", qPrintable(realPath));
418 }
419 } else {
420 QFileInfo fi(realPath);
421 QString parentPath = fi.path();
422 FilesByPath::iterator pIt = watchingState.watchedFiles.find(parentPath);
423 if (pIt != watchingState.watchedFiles.end()) {
424 InfoByName &filesInDir = pIt.value();
425 InfoByName::iterator fIt = filesInDir.find(realPath);
426 if (fIt != filesInDir.end()) {
427 needsRestart |= derefPath(fIt->watchedPath);
428 filesInDir.erase(fIt);
429 if (filesInDir.isEmpty())
430 watchingState.watchedFiles.erase(pIt);
431 files->removeAll(origPath);
432 sg.dismiss();
433 DEBUG("Removed file '%s'", qPrintable(realPath));
434 }
435 }
436 }
437 }
438
439 locker.unlock();
440
441 if (needsRestart) {
442 if (!restartStream()) {
443 watchingState = std::move(oldState);
444 startStream();
445 }
446 }
447
448 return unhandled;
449}
450
451// Returns false if FSEventStream* calls failed for some mysterious reason, true if things got a
452// thumbs-up.
453bool QFseventsFileSystemWatcherEngine::startStream()
454{
455 Q_ASSERT(stream == 0);
456 if (stream) // Ok, this really shouldn't happen, esp. not after the assert. But let's be nice in release mode and still handle it.
457 stopStream();
458
460
461 if (watchingState.watchedPaths.isEmpty())
462 return true; // we succeeded in doing nothing
463
464 DEBUG() << "Starting stream with paths" << watchingState.watchedPaths.keys();
465
466 NSMutableArray<NSString *> *pathsToWatch = [NSMutableArray<NSString *> arrayWithCapacity:watchingState.watchedPaths.size()];
467 for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(), ei = watchingState.watchedPaths.end(); i != ei; ++i)
468 [pathsToWatch addObject:i.key().toNSString()];
469
470 struct FSEventStreamContext callBackInfo = {
471 0,
472 this,
473 NULL,
474 NULL,
475 NULL
476 };
477 const CFAbsoluteTime latency = .5; // in seconds
478
479 // Never start with kFSEventStreamEventIdSinceNow, because this will generate an invalid
480 // soft-assert in FSEventStreamFlushSync in CarbonCore when no event occurred.
481 if (lastReceivedEvent == kFSEventStreamEventIdSinceNow)
482 lastReceivedEvent = FSEventsGetCurrentEventId();
483 stream = FSEventStreamCreate(NULL,
485 &callBackInfo,
486 reinterpret_cast<CFArrayRef>(pathsToWatch),
487 lastReceivedEvent,
488 latency,
489 FSEventStreamCreateFlags(0));
490
491 if (!stream) { // nope, no way to know what went wrong, so just fail
492 DEBUG() << "Failed to create stream!";
493 return false;
494 }
495
496 FSEventStreamSetDispatchQueue(stream, queue);
497
498 if (FSEventStreamStart(stream)) {
499 DEBUG() << "Stream started successfully with sinceWhen =" << lastReceivedEvent;
500 return true;
501 } else { // again, no way to know what went wrong, so just clean up and fail
502 DEBUG() << "Stream failed to start!";
503 FSEventStreamInvalidate(stream);
504 FSEventStreamRelease(stream);
505 stream = 0;
506 return false;
507 }
508}
509
510void QFseventsFileSystemWatcherEngine::stopStream(bool isStopped)
511{
513 if (stream) {
514 if (!isStopped)
515 FSEventStreamStop(stream);
516 FSEventStreamInvalidate(stream);
517 FSEventStreamRelease(stream);
518 stream = 0;
519 DEBUG() << "Stream stopped. Last event ID:" << lastReceivedEvent;
520 }
521}
522
523QFseventsFileSystemWatcherEngine::InfoByName QFseventsFileSystemWatcherEngine::scanForDirEntries(const QString &path)
524{
525 InfoByName entries;
526
528 while (it.hasNext()) {
529 it.next();
530 QString entryName = it.filePath();
531 QT_STATBUF st;
532 if (QT_STAT(QFile::encodeName(entryName), &st) == -1)
533 continue;
534 entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString()));
535 }
536
537 return entries;
538}
539
540bool QFseventsFileSystemWatcherEngine::derefPath(const QString &watchedPath)
541{
542 PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath);
543 if (it != watchingState.watchedPaths.end() && --it.value() < 1) {
544 watchingState.watchedPaths.erase(it);
545 DEBUG("Removing '%s' from watchedPaths.", qPrintable(watchedPath));
546 return true;
547 }
548
549 return false;
550}
551
The QDirIterator class provides an iterator for directory entrylists.
static QChar separator()
Returns the native directory separator: "/" under Unix and "\\" under Windows.
Definition qdir.h:209
QString canonicalFilePath() const
Returns the file system entry's canonical path, including the entry's name, that is,...
void directoryChanged(const QString &path, bool removed)
void fileChanged(const QString &path, bool removed)
static QByteArray encodeName(const QString &fileName)
Converts fileName to an 8-bit encoding that you can use in native APIs.
Definition qfile.h:158
static QString decodeName(const QByteArray &localFileName)
This does the reverse of QFile::encodeName() using localFileName.
Definition qfile.h:162
void processEvent(ConstFSEventStreamRef streamRef, size_t numEvents, char **eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[])
QStringList removePaths(const QStringList &paths, QStringList *files, QStringList *directories)
void emitDirectoryChanged(const QString &path, bool removed)
QStringList addPaths(const QStringList &paths, QStringList *files, QStringList *directories)
static QFseventsFileSystemWatcherEngine * create(QObject *parent)
void emitFileChanged(const QString &path, bool removed)
iterator begin()
Returns an \l{STL-style iterators}{STL-style iterator} pointing to the first item in the hash.
Definition qhash.h:1212
qsizetype size() const noexcept
Returns the number of items in the hash.
Definition qhash.h:927
iterator find(const Key &key)
Returns an iterator pointing to the item with the key in the hash.
Definition qhash.h:1291
friend class iterator
Definition qhash.h:1142
QList< Key > keys() const
Returns a list containing all the keys in the hash, in an arbitrary order.
Definition qhash.h:1086
iterator erase(const_iterator it)
Definition qhash.h:1233
bool contains(const Key &key) const noexcept
Returns true if the hash contains an item with the key; otherwise returns false.
Definition qhash.h:1007
T value(const Key &key) const noexcept
Definition qhash.h:1054
iterator end() noexcept
Returns an \l{STL-style iterators}{STL-style iterator} pointing to the imaginary item after the last ...
Definition qhash.h:1216
friend class const_iterator
Definition qhash.h:1182
bool isEmpty() const noexcept
Returns true if the hash contains no items; otherwise returns false.
Definition qhash.h:928
iterator insert(const Key &key, const T &value)
Inserts a new item with the key and a value of value.
Definition qhash.h:1303
\inmodule QtCore
Definition qmutex.h:313
void unlock() noexcept
Unlocks this mutex locker.
Definition qmutex.h:319
\inmodule QtCore
Definition qobject.h:103
QObject * parent() const
Returns a pointer to the parent object.
Definition qobject.h:346
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2960
iterator begin()
Definition qset.h:136
iterator end()
Definition qset.h:140
iterator erase(const_iterator i)
Definition qset.h:145
iterator insert(const T &value)
Definition qset.h:155
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
bool startsWith(const QString &s, Qt::CaseSensitivity cs=Qt::CaseSensitive) const
Returns true if the string starts with s; otherwise returns false.
Definition qstring.cpp:5455
void chop(qsizetype n)
Removes n characters from the end of the string.
Definition qstring.cpp:6340
QString & insert(qsizetype i, QChar c)
Definition qstring.cpp:3132
@ NormalizationForm_C
Definition qstring.h:619
QString normalized(NormalizationForm mode, QChar::UnicodeVersion version=QChar::Unicode_Unassigned) const
Returns the string in the given Unicode normalization mode, according to the given version of the Uni...
Definition qstring.cpp:8475
QSet< QString >::iterator it
Combined button and popup list for selecting options.
@ QueuedConnection
EGLStreamKHR stream
static QT_BEGIN_NAMESPACE void callBackFunction(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[])
constexpr const T & qMax(const T &a, const T &b)
Definition qminmax.h:42
#define SLOT(a)
Definition qobjectdefs.h:52
#define SIGNAL(a)
Definition qobjectdefs.h:53
GLuint64 key
GLsizei const GLuint * paths
GLuint name
GLuint res
GLsizei const GLchar *const * path
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
QScopeGuard< typename std::decay< F >::type > qScopeGuard(F &&f)
[qScopeGuard]
Definition qscopeguard.h:60
#define qPrintable(string)
Definition qstring.h:1531
#define emit
#define Q_UNUSED(x)
QQueue< int > queue
[0]
QStringList files
[8]
QHostInfo info
[0]
QJSEngine engine
[0]