Warning: That file was not part of the compilation database. It may have many parsing errors.

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

Warning: That file was not part of the compilation database. It may have many parsing errors.