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_MACOS)
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    dispatch_sync(queue, ^{
315        // Stop the stream in case we have to wait for the lock below to be acquired.
316        if (stream)
317            FSEventStreamStop(stream);
318
319        // The assumption with the locking strategy is that this class cannot and will not be subclassed!
320        QMutexLocker locker(&lock);
321
322        stopStream(true);
323    });
324    dispatch_release(queue);
325}
326
327QStringList QFseventsFileSystemWatcherEngine::addPaths(const QStringList &paths,
328                                                       QStringList *files,
329                                                       QStringList *directories)
330{
331    QMacAutoReleasePool pool;
332
333    if (stream) {
334        DEBUG("Flushing, last id is %llu", FSEventStreamGetLatestEventId(stream));
335        FSEventStreamFlushSync(stream);
336    }
337
338    QMutexLocker locker(&lock);
339
340    bool wasRunning = stream != nullptr;
341    bool needsRestart = false;
342
343    WatchingState oldState = watchingState;
344    QStringList unhandled;
345    for (const QString &path : paths) {
346        auto sg = qScopeGuard([&]{ unhandled.push_back(path); });
347        QString origPath = path.normalized(QString::NormalizationForm_C);
348        QString realPath = origPath;
349        if (realPath.endsWith(QDir::separator()))
350            realPath = realPath.mid(0, realPath.size() - 1);
351        QString watchedPath, parentPath;
352
353        realPath = QFileInfo(realPath).canonicalFilePath();
354        QFileInfo fi(realPath);
355        if (realPath.isEmpty())
356            continue;
357
358        QT_STATBUF st;
359        if (QT_STAT(QFile::encodeName(realPath), &st) == -1)
360            continue;
361
362        const bool isDir = S_ISDIR(st.st_mode);
363        if (isDir) {
364            if (watchingState.watchedDirectories.contains(realPath))
365                continue;
366            directories->append(origPath);
367            watchedPath = realPath;
368        } else {
369            if (files->contains(origPath))
370                continue;
371            files->append(origPath);
372
373            watchedPath = fi.path();
374            parentPath = watchedPath;
375        }
376
377        sg.dismiss();
378
379        for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(),
380                ei = watchingState.watchedPaths.end(); i != ei; ++i) {
381            if (watchedPath.startsWith(i.key() % QDir::separator())) {
382                watchedPath = i.key();
383                break;
384            }
385        }
386
387        PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath);
388        if (it == watchingState.watchedPaths.end()) {
389            needsRestart = true;
390            watchingState.watchedPaths.insert(watchedPath, 1);
391            DEBUG("Adding '%s' to watchedPaths", qPrintable(watchedPath));
392        } else {
393            ++it.value();
394        }
395
396        Info info(origPath, st.st_ctimespec, st.st_mode, watchedPath);
397        if (isDir) {
398            DirInfo dirInfo;
399            dirInfo.dirInfo = info;
400            dirInfo.entries = scanForDirEntries(realPath);
401            watchingState.watchedDirectories.insert(realPath, dirInfo);
402            DEBUG("-- Also adding '%s' to watchedDirectories", qPrintable(realPath));
403        } else {
404            watchingState.watchedFiles[parentPath].insert(realPath, info);
405            DEBUG("-- Also adding '%s' to watchedFiles", qPrintable(realPath));
406        }
407    }
408
409    if (needsRestart) {
410        stopStream();
411        if (!startStream()) {
412            // ok, something went wrong, let's try to restore the previous state
413            watchingState = std::move(oldState);
414            // and because we don't know which path caused the issue (if any), fail on all of them
415            unhandled = paths;
416
417            if (wasRunning)
418                startStream();
419        }
420    }
421
422    return unhandled;
423}
424
425QStringList QFseventsFileSystemWatcherEngine::removePaths(const QStringList &paths,
426                                                          QStringList *files,
427                                                          QStringList *directories)
428{
429    QMacAutoReleasePool pool;
430
431    QMutexLocker locker(&lock);
432
433    bool needsRestart = false;
434
435    WatchingState oldState = watchingState;
436    QStringList unhandled;
437    for (const QString &origPath : paths) {
438        auto sg = qScopeGuard([&]{ unhandled.push_back(origPath); });
439        QString realPath = origPath;
440        if (realPath.endsWith(QDir::separator()))
441            realPath = realPath.mid(0, realPath.size() - 1);
442
443        QFileInfo fi(realPath);
444        realPath = fi.canonicalFilePath();
445
446        if (fi.isDir()) {
447            DirsByName::iterator dirIt = watchingState.watchedDirectories.find(realPath);
448            if (dirIt != watchingState.watchedDirectories.end()) {
449                needsRestart |= derefPath(dirIt->dirInfo.watchedPath);
450                watchingState.watchedDirectories.erase(dirIt);
451                directories->removeAll(origPath);
452                sg.dismiss();
453                DEBUG("Removed directory '%s'", qPrintable(realPath));
454            }
455        } else {
456            QFileInfo fi(realPath);
457            QString parentPath = fi.path();
458            FilesByPath::iterator pIt = watchingState.watchedFiles.find(parentPath);
459            if (pIt != watchingState.watchedFiles.end()) {
460                InfoByName &filesInDir = pIt.value();
461                InfoByName::iterator fIt = filesInDir.find(realPath);
462                if (fIt != filesInDir.end()) {
463                    needsRestart |= derefPath(fIt->watchedPath);
464                    filesInDir.erase(fIt);
465                    if (filesInDir.isEmpty())
466                        watchingState.watchedFiles.erase(pIt);
467                    files->removeAll(origPath);
468                    sg.dismiss();
469                    DEBUG("Removed file '%s'", qPrintable(realPath));
470                }
471            }
472        }
473    }
474
475    locker.unlock();
476
477    if (needsRestart) {
478        if (!restartStream()) {
479            watchingState = std::move(oldState);
480            startStream();
481        }
482    }
483
484    return unhandled;
485}
486
487// Returns false if FSEventStream* calls failed for some mysterious reason, true if things got a
488// thumbs-up.
489bool QFseventsFileSystemWatcherEngine::startStream()
490{
491    Q_ASSERT(stream == 0);
492    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.
493        stopStream();
494
495    QMacAutoReleasePool pool;
496
497    if (watchingState.watchedPaths.isEmpty())
498        return true; // we succeeded in doing nothing
499
500    DEBUG() << "Starting stream with paths" << watchingState.watchedPaths.keys();
501
502    NSMutableArray<NSString *> *pathsToWatch = [NSMutableArray<NSString *> arrayWithCapacity:watchingState.watchedPaths.size()];
503    for (PathRefCounts::const_iterator i = watchingState.watchedPaths.begin(), ei = watchingState.watchedPaths.end(); i != ei; ++i)
504        [pathsToWatch addObject:i.key().toNSString()];
505
506    struct FSEventStreamContext callBackInfo = {
507        0,
508        this,
509        NULL,
510        NULL,
511        NULL
512    };
513    const CFAbsoluteTime latency = .5; // in seconds
514
515    // Never start with kFSEventStreamEventIdSinceNow, because this will generate an invalid
516    // soft-assert in FSEventStreamFlushSync in CarbonCore when no event occurred.
517    if (lastReceivedEvent == kFSEventStreamEventIdSinceNow)
518        lastReceivedEvent = FSEventsGetCurrentEventId();
519    stream = FSEventStreamCreate(NULL,
520                                 &callBackFunction,
521                                 &callBackInfo,
522                                 reinterpret_cast<CFArrayRef>(pathsToWatch),
523                                 lastReceivedEvent,
524                                 latency,
525                                 FSEventStreamCreateFlags(0));
526
527    if (!stream) { // nope, no way to know what went wrong, so just fail
528        DEBUG() << "Failed to create stream!";
529        return false;
530    }
531
532    FSEventStreamSetDispatchQueue(stream, queue);
533
534    if (FSEventStreamStart(stream)) {
535        DEBUG() << "Stream started successfully with sinceWhen =" << lastReceivedEvent;
536        return true;
537    } else { // again, no way to know what went wrong, so just clean up and fail
538        DEBUG() << "Stream failed to start!";
539        FSEventStreamInvalidate(stream);
540        FSEventStreamRelease(stream);
541        stream = 0;
542        return false;
543    }
544}
545
546void QFseventsFileSystemWatcherEngine::stopStream(bool isStopped)
547{
548    QMacAutoReleasePool pool;
549    if (stream) {
550        if (!isStopped)
551            FSEventStreamStop(stream);
552        FSEventStreamInvalidate(stream);
553        FSEventStreamRelease(stream);
554        stream = 0;
555        DEBUG() << "Stream stopped. Last event ID:" << lastReceivedEvent;
556    }
557}
558
559QFseventsFileSystemWatcherEngine::InfoByName QFseventsFileSystemWatcherEngine::scanForDirEntries(const QString &path)
560{
561    InfoByName entries;
562
563    QDirIterator it(path);
564    while (it.hasNext()) {
565        it.next();
566        QString entryName = it.filePath();
567        QT_STATBUF st;
568        if (QT_STAT(QFile::encodeName(entryName), &st) == -1)
569            continue;
570        entries.insert(entryName, Info(QString(), st.st_ctimespec, st.st_mode, QString()));
571    }
572
573    return entries;
574}
575
576bool QFseventsFileSystemWatcherEngine::derefPath(const QString &watchedPath)
577{
578    PathRefCounts::iterator it = watchingState.watchedPaths.find(watchedPath);
579    if (it != watchingState.watchedPaths.end() && --it.value() < 1) {
580        watchingState.watchedPaths.erase(it);
581        DEBUG("Removing '%s' from watchedPaths.", qPrintable(watchedPath));
582        return true;
583    }
584
585    return false;
586}
587
588QT_END_NAMESPACE
589