Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
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// Qt-Security score:significant reason:default
4
5#include <qplatformdefs.h>
6
7#include "qdiriterator.h"
10#include "private/qcore_unix_p.h"
11#include "kernel/qcore_mac_p.h"
12
13#include <qdebug.h>
14#include <qdir.h>
15#include <qfile.h>
16#include <qfileinfo.h>
17#include <qvarlengtharray.h>
18#include <qscopeguard.h>
19
20#undef FSEVENT_DEBUG
21#ifdef FSEVENT_DEBUG
22# define DEBUG if (true) qDebug
23#else
24# define DEBUG if (false) qDebug
25#endif
26
28
29static bool isSameTimestampAndMode(const QT_STATBUF &statbuff,
30 const QFseventsFileSystemWatcherEngine::Info &info)
31{
32 const timespec ts = statbuff.st_ctimespec;
33 return (ts.tv_sec == info.ctime.tv_sec && ts.tv_nsec == info.ctime.tv_nsec)
34 && statbuff.st_mode == info.mode;
35}
36
37static void callBackFunction(ConstFSEventStreamRef streamRef,
38 void *clientCallBackInfo,
39 size_t numEvents,
40 void *eventPaths,
41 const FSEventStreamEventFlags eventFlags[],
42 const FSEventStreamEventId eventIds[])
43{
45
46 char **paths = static_cast<char **>(eventPaths);
47 QFseventsFileSystemWatcherEngine *engine = static_cast<QFseventsFileSystemWatcherEngine *>(clientCallBackInfo);
48 engine->processEvent(streamRef, numEvents, paths, eventFlags, eventIds);
49}
50
51bool QFseventsFileSystemWatcherEngine::checkDir(DirsByName::iterator &it)
52{
53 bool needsRestart = false;
54
55 QT_STATBUF st;
56 const QString &name = it.key();
57 Info &info = it->dirInfo;
58 const int res = QT_STAT(QFile::encodeName(name), &st);
59 if (res == -1) {
60 needsRestart |= derefPath(info.watchedPath);
61 emit emitDirectoryChanged(info.origPath, true);
62 it = watchingState.watchedDirectories.erase(it);
63 } else if (!isSameTimestampAndMode(st, info)) {
64 info.ctime = st.st_ctimespec;
65 info.mode = st.st_mode;
66 emit emitDirectoryChanged(info.origPath, false);
67 ++it;
68 } else {
69 bool dirChanged = false;
70 InfoByName &entries = it->entries;
71 // check known entries:
72 for (InfoByName::iterator i = entries.begin(); i != entries.end(); ) {
73 if (QT_STAT(QFile::encodeName(i.key()), &st) == -1) {
74 // entry disappeared
75 dirChanged = true;
76 i = entries.erase(i);
77 } else {
78 if (!isSameTimestampAndMode(st, i.value())) {
79 // entry changed
80 dirChanged = true;
81 i->ctime = st.st_ctimespec;
82 i->mode = st.st_mode;
83 }
84 ++i;
85 }
86 }
87
88 // Ask QDirListing to include hidden entries so it doesn't spend time
89 // trying to figure out if entries are hidden (slow on Darwin due to
90 // kCFURLIsHiddenKey check).
91 QDirListing::IteratorFlags flags = QDirListing::IteratorFlag::IncludeHidden;
92
93 // check for new entries:
94 QDirListing listing(name, flags);
95 for (auto dirIt : listing) {
96 QString entryName = dirIt.filePath();
97 if (!entries.contains(entryName)) {
98 dirChanged = true;
99 QT_STATBUF st;
100 if (QT_STAT(QFile::encodeName(entryName), &st) == -1)
101 continue;
102 entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString()));
103
104 }
105 }
106 if (dirChanged)
107 emit emitDirectoryChanged(info.origPath, false);
108 ++it;
109 }
110
111 return needsRestart;
112}
113
114bool QFseventsFileSystemWatcherEngine::rescanDirs(const QString &path)
115{
116 bool needsRestart = false;
117
118 for (DirsByName::iterator it = watchingState.watchedDirectories.begin();
119 it != watchingState.watchedDirectories.end(); ) {
120 if (it.key().startsWith(path))
121 needsRestart |= checkDir(it);
122 else
123 ++it;
124 }
125
126 return needsRestart;
127}
128
129bool QFseventsFileSystemWatcherEngine::rescanFiles(InfoByName &filesInPath)
130{
131 bool needsRestart = false;
132
133 for (InfoByName::iterator it = filesInPath.begin(); it != filesInPath.end(); ) {
134 QT_STATBUF st;
135 QString name = it.key();
136 const int res = QT_STAT(QFile::encodeName(name), &st);
137 if (res == -1) {
138 needsRestart |= derefPath(it->watchedPath);
139 emit emitFileChanged(it.value().origPath, true);
140 it = filesInPath.erase(it);
141 continue;
142 } else if (!isSameTimestampAndMode(st, it.value())) {
143 it->ctime = st.st_ctimespec;
144 it->mode = st.st_mode;
145 emit emitFileChanged(it.value().origPath, false);
146 }
147
148 ++it;
149 }
150
151 return needsRestart;
152}
153
154bool QFseventsFileSystemWatcherEngine::rescanFiles(const QString &path)
155{
156 bool needsRestart = false;
157
158 for (FilesByPath::iterator i = watchingState.watchedFiles.begin();
159 i != watchingState.watchedFiles.end(); ) {
160 if (i.key().startsWith(path)) {
161 needsRestart |= rescanFiles(i.value());
162 if (i.value().isEmpty()) {
163 i = watchingState.watchedFiles.erase(i);
164 continue;
165 }
166 }
167
168 ++i;
169 }
170
171 return needsRestart;
172}
173
174void QFseventsFileSystemWatcherEngine::processEvent(ConstFSEventStreamRef streamRef,
175 size_t numEvents,
176 char **eventPaths,
177 const FSEventStreamEventFlags eventFlags[],
178 const FSEventStreamEventId eventIds[])
179{
180#if defined(Q_OS_MACOS)
181 Q_UNUSED(streamRef);
182
183 bool needsRestart = false;
184
185 QMutexLocker locker(&lock);
186
187 for (size_t i = 0; i < numEvents; ++i) {
188 FSEventStreamEventFlags eFlags = eventFlags[i];
189 DEBUG("Change %llu in %s, flags %x", eventIds[i], eventPaths[i], (unsigned int)eFlags);
190
191 if (eFlags & kFSEventStreamEventFlagEventIdsWrapped) {
192 DEBUG("\tthe event ids wrapped");
193 lastReceivedEvent = 0;
194 }
195 lastReceivedEvent = qMax(lastReceivedEvent, eventIds[i]);
196
197 QString path = QFile::decodeName(eventPaths[i]);
198 if (path.size() > 1 && path.endsWith(QDir::separator()))
199 path.chop(1);
200
201 if (eFlags & kFSEventStreamEventFlagMustScanSubDirs) {
202 DEBUG("\tmust rescan directory because of coalesced events");
203 if (eFlags & kFSEventStreamEventFlagUserDropped)
204 DEBUG("\t\t... user dropped.");
205 if (eFlags & kFSEventStreamEventFlagKernelDropped)
206 DEBUG("\t\t... kernel dropped.");
207 needsRestart |= rescanDirs(path);
208 needsRestart |= rescanFiles(path);
209 continue;
210 }
211
212 if (eFlags & kFSEventStreamEventFlagRootChanged) {
213 // re-check everything:
214 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(path);
215 if (dirIt != watchingState.watchedDirectories.end())
216 needsRestart |= checkDir(dirIt);
217 needsRestart |= rescanFiles(path);
218 continue;
219 }
220
221 if ((eFlags & kFSEventStreamEventFlagItemIsDir) && (eFlags & kFSEventStreamEventFlagItemRemoved))
222 needsRestart |= rescanDirs(path);
223
224 // check watched directories:
225 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(path);
226 if (dirIt != watchingState.watchedDirectories.end())
227 needsRestart |= checkDir(dirIt);
228
229 // check watched files:
230 FilesByPath::iterator pIt = watchingState.watchedFiles.find(path);
231 if (pIt != watchingState.watchedFiles.end())
232 needsRestart |= rescanFiles(pIt.value());
233 }
234
235 if (needsRestart)
236 emit scheduleStreamRestart();
237#else
238 Q_UNUSED(streamRef);
239 Q_UNUSED(numEvents);
240 Q_UNUSED(eventPaths);
241 Q_UNUSED(eventFlags);
242 Q_UNUSED(eventIds);
243#endif
244}
245
246void QFseventsFileSystemWatcherEngine::doEmitFileChanged(const QString &path, bool removed)
247{
248 DEBUG() << "emitting fileChanged for" << path << "with removed =" << removed;
249 emit fileChanged(path, removed);
250}
251
252void QFseventsFileSystemWatcherEngine::doEmitDirectoryChanged(const QString &path, bool removed)
253{
254 DEBUG() << "emitting directoryChanged for" << path << "with removed =" << removed;
255 emit directoryChanged(path, removed);
256}
257
258bool QFseventsFileSystemWatcherEngine::restartStream()
259{
260 QMutexLocker locker(&lock);
261 stopStream();
262 return startStream();
263}
264
265QFseventsFileSystemWatcherEngine *QFseventsFileSystemWatcherEngine::create(QObject *parent)
266{
267 return new QFseventsFileSystemWatcherEngine(parent);
268}
269
270QFseventsFileSystemWatcherEngine::QFseventsFileSystemWatcherEngine(QObject *parent)
271 : QFileSystemWatcherEngine(parent)
272 , stream(0)
273 , lastReceivedEvent(kFSEventStreamEventIdSinceNow)
274{
275
276 // We cannot use signal-to-signal queued connections, because the
277 // QSignalSpy cannot spot signals fired from other/alien threads.
278 connect(this, SIGNAL(emitDirectoryChanged(QString,bool)),
279 this, SLOT(doEmitDirectoryChanged(QString,bool)), Qt::QueuedConnection);
280 connect(this, SIGNAL(emitFileChanged(QString,bool)),
281 this, SLOT(doEmitFileChanged(QString,bool)), Qt::QueuedConnection);
282 connect(this, SIGNAL(scheduleStreamRestart()),
283 this, SLOT(restartStream()), Qt::QueuedConnection);
284
285 queue = dispatch_queue_create("org.qt-project.QFseventsFileSystemWatcherEngine", NULL);
286}
287
288QFseventsFileSystemWatcherEngine::~QFseventsFileSystemWatcherEngine()
289{
291
292 dispatch_sync(queue, ^{
293 // Stop the stream in case we have to wait for the lock below to be acquired.
294 if (stream)
295 FSEventStreamStop(stream);
296
297 // The assumption with the locking strategy is that this class cannot and will not be subclassed!
298 QMutexLocker locker(&lock);
299
300 stopStream(true);
301 });
302 dispatch_release(queue);
303}
304
305QStringList QFseventsFileSystemWatcherEngine::addPaths(const QStringList &paths,
306 QStringList *files,
307 QStringList *directories)
308{
310
311 if (stream) {
312 DEBUG("Flushing, last id is %llu", FSEventStreamGetLatestEventId(stream));
313 FSEventStreamFlushSync(stream);
314 }
315
316 QMutexLocker locker(&lock);
317
318 bool wasRunning = stream != nullptr;
319 bool needsRestart = false;
320
321 WatchingState oldState = watchingState;
322 QStringList unhandled;
323 for (const QString &path : paths) {
324 auto sg = qScopeGuard([&]{ unhandled.push_back(path); });
325 QString origPath = path.normalized(QString::NormalizationForm_C);
326 QString realPath = origPath;
327 if (realPath.size() > 1 && realPath.endsWith(QDir::separator()))
328 realPath.chop(1);
329 QString watchedPath, parentPath;
330
331 realPath = QFileInfo(realPath).canonicalFilePath();
332 QFileInfo fi(realPath);
333 if (realPath.isEmpty())
334 continue;
335
336 QT_STATBUF st;
337 if (QT_STAT(QFile::encodeName(realPath), &st) == -1)
338 continue;
339
340 const bool isDir = S_ISDIR(st.st_mode);
341 if (isDir) {
342 if (watchingState.watchedDirectories.contains(realPath))
343 continue;
344 directories->append(origPath);
345 watchedPath = realPath;
346 } else {
347 if (files->contains(origPath))
348 continue;
349 files->append(origPath);
350
351 watchedPath = fi.path();
352 parentPath = watchedPath;
353 }
354
355 sg.dismiss();
356
357 for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(),
358 ei = watchingState.watchedPaths.end(); i != ei; ++i) {
359 if (watchedPath.startsWith(i.key() % QDir::separator())) {
360 watchedPath = i.key();
361 break;
362 }
363 }
364
365 PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath);
366 if (it == watchingState.watchedPaths.end()) {
367 needsRestart = true;
368 watchingState.watchedPaths.insert(watchedPath, 1);
369 DEBUG("Adding '%s' to watchedPaths", qPrintable(watchedPath));
370 } else {
371 ++it.value();
372 }
373
374 Info info(origPath, st.st_ctimespec, st.st_mode, watchedPath);
375 if (isDir) {
376 DirInfo dirInfo;
377 dirInfo.dirInfo = info;
378 dirInfo.entries = scanForDirEntries(realPath);
379 watchingState.watchedDirectories.insert(realPath, dirInfo);
380 DEBUG("-- Also adding '%s' to watchedDirectories", qPrintable(realPath));
381 } else {
382 watchingState.watchedFiles[parentPath].insert(realPath, info);
383 DEBUG("-- Also adding '%s' to watchedFiles", qPrintable(realPath));
384 }
385 }
386
387 if (needsRestart) {
388 stopStream();
389 if (!startStream()) {
390 // ok, something went wrong, let's try to restore the previous state
391 watchingState = std::move(oldState);
392 // and because we don't know which path caused the issue (if any), fail on all of them
393 unhandled = paths;
394
395 if (wasRunning)
396 startStream();
397 }
398 }
399
400 return unhandled;
401}
402
403QStringList QFseventsFileSystemWatcherEngine::removePaths(const QStringList &paths,
404 QStringList *files,
405 QStringList *directories)
406{
408
409 QMutexLocker locker(&lock);
410
411 bool needsRestart = false;
412
413 WatchingState oldState = watchingState;
414 QStringList unhandled;
415 for (const QString &origPath : paths) {
416 auto sg = qScopeGuard([&]{ unhandled.push_back(origPath); });
417 QString realPath = origPath;
418 if (realPath.size() > 1 && realPath.endsWith(QDir::separator()))
419 realPath.chop(1);
420
421 QFileInfo fi(realPath);
422 realPath = fi.canonicalFilePath();
423
424 if (fi.isDir()) {
425 DirsByName::iterator dirIt = watchingState.watchedDirectories.find(realPath);
426 if (dirIt != watchingState.watchedDirectories.end()) {
427 needsRestart |= derefPath(dirIt->dirInfo.watchedPath);
428 watchingState.watchedDirectories.erase(dirIt);
429 directories->removeAll(origPath);
430 sg.dismiss();
431 DEBUG("Removed directory '%s'", qPrintable(realPath));
432 }
433 } else {
434 QFileInfo fi(realPath);
435 QString parentPath = fi.path();
436 FilesByPath::iterator pIt = watchingState.watchedFiles.find(parentPath);
437 if (pIt != watchingState.watchedFiles.end()) {
438 InfoByName &filesInDir = pIt.value();
439 InfoByName::iterator fIt = filesInDir.find(realPath);
440 if (fIt != filesInDir.end()) {
441 needsRestart |= derefPath(fIt->watchedPath);
442 filesInDir.erase(fIt);
443 if (filesInDir.isEmpty())
444 watchingState.watchedFiles.erase(pIt);
445 files->removeAll(origPath);
446 sg.dismiss();
447 DEBUG("Removed file '%s'", qPrintable(realPath));
448 }
449 }
450 }
451 }
452
453 locker.unlock();
454
455 if (needsRestart) {
456 if (!restartStream()) {
457 watchingState = std::move(oldState);
458 startStream();
459 }
460 }
461
462 return unhandled;
463}
464
465// Returns false if FSEventStream* calls failed for some mysterious reason, true if things got a
466// thumbs-up.
467bool QFseventsFileSystemWatcherEngine::startStream()
468{
469 Q_ASSERT(stream == 0);
470 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.
471 stopStream();
472
474
475 if (watchingState.watchedPaths.isEmpty())
476 return true; // we succeeded in doing nothing
477
478 DEBUG() << "Starting stream with paths" << watchingState.watchedPaths.keys();
479
480 NSMutableArray<NSString *> *pathsToWatch = [NSMutableArray<NSString *> arrayWithCapacity:watchingState.watchedPaths.size()];
481 for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(), ei = watchingState.watchedPaths.end(); i != ei; ++i)
482 [pathsToWatch addObject:i.key().toNSString()];
483
484 struct FSEventStreamContext callBackInfo = {
485 0,
486 this,
487 NULL,
488 NULL,
489 NULL
490 };
491 const CFAbsoluteTime latency = .5; // in seconds
492
493 // Never start with kFSEventStreamEventIdSinceNow, because this will generate an invalid
494 // soft-assert in FSEventStreamFlushSync in CarbonCore when no event occurred.
495 if (lastReceivedEvent == kFSEventStreamEventIdSinceNow)
496 lastReceivedEvent = FSEventsGetCurrentEventId();
497 stream = FSEventStreamCreate(NULL,
498 &callBackFunction,
499 &callBackInfo,
500 reinterpret_cast<CFArrayRef>(pathsToWatch),
501 lastReceivedEvent,
502 latency,
503 FSEventStreamCreateFlags(0));
504
505 if (!stream) { // nope, no way to know what went wrong, so just fail
506 DEBUG() << "Failed to create stream!";
507 return false;
508 }
509
510 FSEventStreamSetDispatchQueue(stream, queue);
511
512 if (FSEventStreamStart(stream)) {
513 DEBUG() << "Stream started successfully with sinceWhen =" << lastReceivedEvent;
514 return true;
515 } else { // again, no way to know what went wrong, so just clean up and fail
516 DEBUG() << "Stream failed to start!";
517 FSEventStreamInvalidate(stream);
518 FSEventStreamRelease(stream);
519 stream = 0;
520 return false;
521 }
522}
523
524void QFseventsFileSystemWatcherEngine::stopStream(bool isStopped)
525{
527 if (stream) {
528 if (!isStopped)
529 FSEventStreamStop(stream);
530 FSEventStreamInvalidate(stream);
531 FSEventStreamRelease(stream);
532 stream = 0;
533 DEBUG() << "Stream stopped. Last event ID:" << lastReceivedEvent;
534 }
535}
536
537QFseventsFileSystemWatcherEngine::InfoByName QFseventsFileSystemWatcherEngine::scanForDirEntries(const QString &path)
538{
539 InfoByName entries;
540
541 QDirIterator it(path);
542 while (it.hasNext()) {
543 it.next();
544 QString entryName = it.filePath();
545 QT_STATBUF st;
546 if (QT_STAT(QFile::encodeName(entryName), &st) == -1)
547 continue;
548 entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString()));
549 }
550
551 return entries;
552}
553
554bool QFseventsFileSystemWatcherEngine::derefPath(const QString &watchedPath)
555{
556 PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath);
557 if (it != watchingState.watchedPaths.end() && --it.value() < 1) {
558 watchingState.watchedPaths.erase(it);
559 DEBUG("Removing '%s' from watchedPaths.", qPrintable(watchedPath));
560 return true;
561 }
562
563 return false;
564}
565
566QT_END_NAMESPACE
Combined button and popup list for selecting options.
const QString & asString(const QString &s)
Definition qstring.h:1678
static void callBackFunction(ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[])
static QT_BEGIN_NAMESPACE bool isSameTimestampAndMode(const QT_STATBUF &statbuff, const QFseventsFileSystemWatcherEngine::Info &info)
#define qPrintable(string)
Definition qstring.h:1683