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