1 /****************************************************************************
2 **
3 ** Copyright (C) 2015 The Qt Company Ltd.
4 ** Contact: http://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 http://www.qt.io/terms-conditions. For further
15 ** information use the contact form at http://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 2.1 or version 3 as published by the Free
20 ** Software Foundation and appearing in the file LICENSE.LGPLv21 and
21 ** LICENSE.LGPLv3 included in the packaging of this file. Please review the
22 ** following information to ensure the GNU Lesser General Public License
23 ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
24 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 **
26 ** As a special exception, The Qt Company gives you certain additional
27 ** rights. These rights are described in The Qt Company LGPL Exception
28 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 **
30 ** GNU General Public License Usage
31 ** Alternatively, this file may be used under the terms of the GNU
32 ** General Public License version 3.0 as published by the Free Software
33 ** Foundation and appearing in the file LICENSE.GPL included in the
34 ** packaging of this file.  Please review the following information to
35 ** ensure the GNU General Public License version 3.0 requirements will be
36 ** met: http://www.gnu.org/copyleft/gpl.html.
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41 
42 #include "qfilesystemwatcher.h"
43 #include "qfilesystemwatcher_p.h"
44 
45 #ifndef QT_NO_FILESYSTEMWATCHER
46 
47 #include <qdatetime.h>
48 #include <qdebug.h>
49 #include <qdir.h>
50 #include <qfileinfo.h>
51 #include <qmutex.h>
52 #include <qset.h>
53 #include <qtimer.h>
54 
55 #if defined(Q_OS_WIN)
56 #  include "qfilesystemwatcher_win_p.h"
57 #elif defined(Q_OS_LINUX)
58 #  include "qfilesystemwatcher_inotify_p.h"
59 #  include "qfilesystemwatcher_dnotify_p.h"
60 #elif defined(Q_OS_QNX) && !defined(QT_NO_INOTIFY)
61 #  include "qfilesystemwatcher_inotify_p.h"
62 #elif defined(Q_OS_FREEBSD) || defined(Q_OS_MAC)
63 #  if (defined Q_OS_MAC) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
64 #  include "qfilesystemwatcher_fsevents_p.h"
65 #  endif //MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
66 #  include "qfilesystemwatcher_kqueue_p.h"
67 #elif defined(Q_OS_SYMBIAN)
68 #  include "qfilesystemwatcher_symbian_p.h"
69 #endif
70 
71 QT_BEGIN_NAMESPACE
72 
73 enum { PollingInterval = 1000 };
74 
75 class QPollingFileSystemWatcherEngine : public QFileSystemWatcherEngine
76 {
77     Q_OBJECT
78 
79     class FileInfo
80     {
81         uint ownerId;
82         uint groupId;
83         QFile::Permissions permissions;
84         QDateTime lastModified;
85         QStringList entries;
86 
87     public:
FileInfo(const QFileInfo & fileInfo)88         FileInfo(const QFileInfo &fileInfo)
89             : ownerId(fileInfo.ownerId()),
90               groupId(fileInfo.groupId()),
91               permissions(fileInfo.permissions()),
92               lastModified(fileInfo.lastModified())
93         {
94             if (fileInfo.isDir()) {
95                 entries = fileInfo.absoluteDir().entryList(QDir::AllEntries);
96             }
97         }
operator =(const QFileInfo & fileInfo)98         FileInfo &operator=(const QFileInfo &fileInfo)
99         {
100             *this = FileInfo(fileInfo);
101             return *this;
102         }
103 
operator !=(const QFileInfo & fileInfo) const104         bool operator!=(const QFileInfo &fileInfo) const
105         {
106             if (fileInfo.isDir() && entries != fileInfo.absoluteDir().entryList(QDir::AllEntries))
107                 return true;
108             return (ownerId != fileInfo.ownerId()
109                     || groupId != fileInfo.groupId()
110                     || permissions != fileInfo.permissions()
111                     || lastModified != fileInfo.lastModified());
112         }
113     };
114 
115     mutable QMutex mutex;
116     QHash<QString, FileInfo> files, directories;
117 
118 public:
119     QPollingFileSystemWatcherEngine();
120 
121     void run();
122 
123     QStringList addPaths(const QStringList &paths, QStringList *files, QStringList *directories);
124     QStringList removePaths(const QStringList &paths, QStringList *files, QStringList *directories);
125 
126     void stop();
127 
128 private Q_SLOTS:
129     void timeout();
130 };
131 
QPollingFileSystemWatcherEngine()132 QPollingFileSystemWatcherEngine::QPollingFileSystemWatcherEngine()
133 {
134 #ifndef QT_NO_THREAD
135     moveToThread(this);
136 #endif
137 }
138 
run()139 void QPollingFileSystemWatcherEngine::run()
140 {
141     QTimer timer;
142     connect(&timer, SIGNAL(timeout()), SLOT(timeout()));
143     timer.start(PollingInterval);
144     (void) exec();
145 }
146 
addPaths(const QStringList & paths,QStringList * files,QStringList * directories)147 QStringList QPollingFileSystemWatcherEngine::addPaths(const QStringList &paths,
148                                                       QStringList *files,
149                                                       QStringList *directories)
150 {
151     QMutexLocker locker(&mutex);
152     QStringList p = paths;
153     QMutableListIterator<QString> it(p);
154     while (it.hasNext()) {
155         QString path = it.next();
156         QFileInfo fi(path);
157         if (!fi.exists())
158             continue;
159         if (fi.isDir()) {
160             if (!directories->contains(path))
161                 directories->append(path);
162             if (!path.endsWith(QLatin1Char('/')))
163                 fi = QFileInfo(path + QLatin1Char('/'));
164             this->directories.insert(path, fi);
165         } else {
166             if (!files->contains(path))
167                 files->append(path);
168             this->files.insert(path, fi);
169         }
170         it.remove();
171     }
172     start();
173     return p;
174 }
175 
removePaths(const QStringList & paths,QStringList * files,QStringList * directories)176 QStringList QPollingFileSystemWatcherEngine::removePaths(const QStringList &paths,
177                                                          QStringList *files,
178                                                          QStringList *directories)
179 {
180     QMutexLocker locker(&mutex);
181     QStringList p = paths;
182     QMutableListIterator<QString> it(p);
183     while (it.hasNext()) {
184         QString path = it.next();
185         if (this->directories.remove(path)) {
186             directories->removeAll(path);
187             it.remove();
188         } else if (this->files.remove(path)) {
189             files->removeAll(path);
190             it.remove();
191         }
192     }
193     if (this->files.isEmpty() && this->directories.isEmpty()) {
194         locker.unlock();
195         stop();
196         wait();
197     }
198     return p;
199 }
200 
stop()201 void QPollingFileSystemWatcherEngine::stop()
202 {
203     quit();
204 }
205 
timeout()206 void QPollingFileSystemWatcherEngine::timeout()
207 {
208     QMutexLocker locker(&mutex);
209     QMutableHashIterator<QString, FileInfo> fit(files);
210     while (fit.hasNext()) {
211         QHash<QString, FileInfo>::iterator x = fit.next();
212         QString path = x.key();
213         QFileInfo fi(path);
214         if (!fi.exists()) {
215             fit.remove();
216             emit fileChanged(path, true);
217         } else if (x.value() != fi) {
218             x.value() = fi;
219             emit fileChanged(path, false);
220         }
221     }
222     QMutableHashIterator<QString, FileInfo> dit(directories);
223     while (dit.hasNext()) {
224         QHash<QString, FileInfo>::iterator x = dit.next();
225         QString path = x.key();
226         QFileInfo fi(path);
227         if (!path.endsWith(QLatin1Char('/')))
228             fi = QFileInfo(path + QLatin1Char('/'));
229         if (!fi.exists()) {
230             dit.remove();
231             emit directoryChanged(path, true);
232         } else if (x.value() != fi) {
233             fi.refresh();
234             if (!fi.exists()) {
235                 dit.remove();
236                 emit directoryChanged(path, true);
237             } else {
238                 x.value() = fi;
239                 emit directoryChanged(path, false);
240             }
241         }
242 
243     }
244 }
245 
246 
247 
248 
createNativeEngine()249 QFileSystemWatcherEngine *QFileSystemWatcherPrivate::createNativeEngine()
250 {
251 #if defined(Q_OS_WIN)
252     return new QWindowsFileSystemWatcherEngine;
253 #elif defined(Q_OS_QNX) && !defined(QT_NO_INOTIFY)
254     return QInotifyFileSystemWatcherEngine::create();
255 #elif defined(Q_OS_LINUX)
256     QFileSystemWatcherEngine *eng = QInotifyFileSystemWatcherEngine::create();
257     if(!eng)
258         eng = QDnotifyFileSystemWatcherEngine::create();
259     return eng;
260 #elif defined(Q_OS_FREEBSD) || defined(Q_OS_MAC)
261 #  if 0 && defined(Q_OS_MAC) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5)
262     if (QSysInfo::MacintoshVersion >= QSysInfo::MV_10_5)
263         return QFSEventsFileSystemWatcherEngine::create();
264     else
265 #  endif
266         return QKqueueFileSystemWatcherEngine::create();
267 #elif defined(Q_OS_SYMBIAN)
268     return new QSymbianFileSystemWatcherEngine;
269 #else
270     return 0;
271 #endif
272 }
273 
QFileSystemWatcherPrivate()274 QFileSystemWatcherPrivate::QFileSystemWatcherPrivate()
275     : native(0), poller(0), forced(0)
276 {
277 }
278 
init()279 void QFileSystemWatcherPrivate::init()
280 {
281     Q_Q(QFileSystemWatcher);
282     native = createNativeEngine();
283     if (native) {
284         QObject::connect(native,
285                          SIGNAL(fileChanged(QString,bool)),
286                          q,
287                          SLOT(_q_fileChanged(QString,bool)));
288         QObject::connect(native,
289                          SIGNAL(directoryChanged(QString,bool)),
290                          q,
291                          SLOT(_q_directoryChanged(QString,bool)));
292     }
293 }
294 
initForcedEngine(const QString & forceName)295 void QFileSystemWatcherPrivate::initForcedEngine(const QString &forceName)
296 {
297     if(forced)
298         return;
299 
300     Q_Q(QFileSystemWatcher);
301 
302 #if defined(Q_OS_LINUX)
303     if(forceName == QLatin1String("inotify")) {
304         forced = QInotifyFileSystemWatcherEngine::create();
305     } else if(forceName == QLatin1String("dnotify")) {
306         forced = QDnotifyFileSystemWatcherEngine::create();
307     }
308 #else
309     Q_UNUSED(forceName);
310 #endif
311 
312     if(forced) {
313         QObject::connect(forced,
314                          SIGNAL(fileChanged(QString,bool)),
315                          q,
316                          SLOT(_q_fileChanged(QString,bool)));
317         QObject::connect(forced,
318                          SIGNAL(directoryChanged(QString,bool)),
319                          q,
320                          SLOT(_q_directoryChanged(QString,bool)));
321     }
322 }
323 
initPollerEngine()324 void QFileSystemWatcherPrivate::initPollerEngine()
325 {
326     if(poller)
327         return;
328 
329     Q_Q(QFileSystemWatcher);
330     poller = new QPollingFileSystemWatcherEngine; // that was a mouthful
331     QObject::connect(poller,
332                      SIGNAL(fileChanged(QString,bool)),
333                      q,
334                      SLOT(_q_fileChanged(QString,bool)));
335     QObject::connect(poller,
336                      SIGNAL(directoryChanged(QString,bool)),
337                      q,
338                      SLOT(_q_directoryChanged(QString,bool)));
339 }
340 
_q_fileChanged(const QString & path,bool removed)341 void QFileSystemWatcherPrivate::_q_fileChanged(const QString &path, bool removed)
342 {
343     Q_Q(QFileSystemWatcher);
344     if (!files.contains(path)) {
345         // the path was removed after a change was detected, but before we delivered the signal
346         return;
347     }
348     if (removed)
349         files.removeAll(path);
350     emit q->fileChanged(path);
351 }
352 
_q_directoryChanged(const QString & path,bool removed)353 void QFileSystemWatcherPrivate::_q_directoryChanged(const QString &path, bool removed)
354 {
355     Q_Q(QFileSystemWatcher);
356     if (!directories.contains(path)) {
357         // perhaps the path was removed after a change was detected, but before we delivered the signal
358         return;
359     }
360     if (removed)
361         directories.removeAll(path);
362     emit q->directoryChanged(path);
363 }
364 
365 
366 
367 /*!
368     \class QFileSystemWatcher
369     \brief The QFileSystemWatcher class provides an interface for monitoring files and directories for modifications.
370     \ingroup io
371     \since 4.2
372     \reentrant
373 
374     QFileSystemWatcher monitors the file system for changes to files
375     and directories by watching a list of specified paths.
376 
377     Call addPath() to watch a particular file or directory. Multiple
378     paths can be added using the addPaths() function. Existing paths can
379     be removed by using the removePath() and removePaths() functions.
380 
381     QFileSystemWatcher examines each path added to it. Files that have
382     been added to the QFileSystemWatcher can be accessed using the
383     files() function, and directories using the directories() function.
384 
385     The fileChanged() signal is emitted when a file has been modified,
386     renamed or removed from disk. Similarly, the directoryChanged()
387     signal is emitted when a directory or its contents is modified or
388     removed.  Note that QFileSystemWatcher stops monitoring files once
389     they have been renamed or removed from disk, and directories once
390     they have been removed from disk.
391 
392     \note On systems running a Linux kernel without inotify support,
393     file systems that contain watched paths cannot be unmounted.
394 
395     \note Windows CE does not support directory monitoring by
396     default as this depends on the file system driver installed.
397 
398     \note The act of monitoring files and directories for
399     modifications consumes system resources. This implies there is a
400     limit to the number of files and directories your process can
401     monitor simultaneously. On Mac OS X 10.4 and all BSD variants, for
402     example, an open file descriptor is required for each monitored
403     file. Some system limits the number of open file descriptors to 256
404     by default. This means that addPath() and addPaths() will fail if
405     your process tries to add more than 256 files or directories to
406     the file system monitor. Also note that your process may have
407     other file descriptors open in addition to the ones for files
408     being monitored, and these other open descriptors also count in
409     the total. Mac OS X 10.5 and up use a different backend and do not
410     suffer from this issue.
411 
412 
413     \sa QFile, QDir
414 */
415 
416 
417 /*!
418     Constructs a new file system watcher object with the given \a parent.
419 */
QFileSystemWatcher(QObject * parent)420 QFileSystemWatcher::QFileSystemWatcher(QObject *parent)
421     : QObject(*new QFileSystemWatcherPrivate, parent)
422 {
423     d_func()->init();
424 }
425 
426 /*!
427     Constructs a new file system watcher object with the given \a parent
428     which monitors the specified \a paths list.
429 */
QFileSystemWatcher(const QStringList & paths,QObject * parent)430 QFileSystemWatcher::QFileSystemWatcher(const QStringList &paths, QObject *parent)
431     : QObject(*new QFileSystemWatcherPrivate, parent)
432 {
433     d_func()->init();
434     addPaths(paths);
435 }
436 
437 /*!
438     Destroys the file system watcher.
439 */
~QFileSystemWatcher()440 QFileSystemWatcher::~QFileSystemWatcher()
441 {
442     Q_D(QFileSystemWatcher);
443     if (d->native) {
444         d->native->stop();
445         d->native->wait();
446         delete d->native;
447         d->native = 0;
448     }
449     if (d->poller) {
450         d->poller->stop();
451         d->poller->wait();
452         delete d->poller;
453         d->poller = 0;
454     }
455     if (d->forced) {
456         d->forced->stop();
457         d->forced->wait();
458         delete d->forced;
459         d->forced = 0;
460     }
461 }
462 
463 /*!
464     Adds \a path to the file system watcher if \a path exists. The
465     path is not added if it does not exist, or if it is already being
466     monitored by the file system watcher.
467 
468     If \a path specifies a directory, the directoryChanged() signal
469     will be emitted when \a path is modified or removed from disk;
470     otherwise the fileChanged() signal is emitted when \a path is
471     modified, renamed or removed.
472 
473     \note There is a system dependent limit to the number of files and
474     directories that can be monitored simultaneously. If this limit
475     has been reached, \a path will not be added to the file system
476     watcher, and a warning message will be printed to \e{stderr}.
477 
478     \sa addPaths(), removePath()
479 */
addPath(const QString & path)480 void QFileSystemWatcher::addPath(const QString &path)
481 {
482     if (path.isEmpty()) {
483         qWarning("QFileSystemWatcher::addPath: path is empty");
484         return;
485     }
486     addPaths(QStringList(path));
487 }
488 
489 /*!
490     Adds each path in \a paths to the file system watcher. Paths are
491     not added if they not exist, or if they are already being
492     monitored by the file system watcher.
493 
494     If a path specifies a directory, the directoryChanged() signal
495     will be emitted when the path is modified or removed from disk;
496     otherwise the fileChanged() signal is emitted when the path is
497     modified, renamed, or removed.
498 
499     \note There is a system dependent limit to the number of files and
500     directories that can be monitored simultaneously. If this limit
501     has been reached, the excess \a paths will not be added to the
502     file system watcher, and a warning message will be printed to
503     \e{stderr} for each path that could not be added.
504 
505     \sa addPath(), removePaths()
506 */
addPaths(const QStringList & paths)507 void QFileSystemWatcher::addPaths(const QStringList &paths)
508 {
509     Q_D(QFileSystemWatcher);
510     if (paths.isEmpty()) {
511         qWarning("QFileSystemWatcher::addPaths: list is empty");
512         return;
513     }
514 
515     QStringList p = paths;
516     QFileSystemWatcherEngine *engine = 0;
517 
518     if(!objectName().startsWith(QLatin1String("_qt_autotest_force_engine_"))) {
519         // Normal runtime case - search intelligently for best engine
520         if(d->native) {
521             engine = d->native;
522         } else {
523             d_func()->initPollerEngine();
524             engine = d->poller;
525         }
526 
527     } else {
528         // Autotest override case - use the explicitly selected engine only
529         QString forceName = objectName().mid(26);
530         if(forceName == QLatin1String("poller")) {
531             qDebug() << "QFileSystemWatcher: skipping native engine, using only polling engine";
532             d_func()->initPollerEngine();
533             engine = d->poller;
534         } else if(forceName == QLatin1String("native")) {
535             qDebug() << "QFileSystemWatcher: skipping polling engine, using only native engine";
536             engine = d->native;
537         } else {
538             qDebug() << "QFileSystemWatcher: skipping polling and native engine, using only explicit" << forceName << "engine";
539             d_func()->initForcedEngine(forceName);
540             engine = d->forced;
541         }
542     }
543 
544     if(engine)
545         p = engine->addPaths(p, &d->files, &d->directories);
546 
547     if (!p.isEmpty())
548         qWarning("QFileSystemWatcher: failed to add paths: %s",
549                  qPrintable(p.join(QLatin1String(", "))));
550 }
551 
552 /*!
553     Removes the specified \a path from the file system watcher.
554 
555     \sa removePaths(), addPath()
556 */
removePath(const QString & path)557 void QFileSystemWatcher::removePath(const QString &path)
558 {
559     if (path.isEmpty()) {
560         qWarning("QFileSystemWatcher::removePath: path is empty");
561         return;
562     }
563     removePaths(QStringList(path));
564 }
565 
566 /*!
567     Removes the specified \a paths from the file system watcher.
568 
569     \sa removePath(), addPaths()
570 */
removePaths(const QStringList & paths)571 void QFileSystemWatcher::removePaths(const QStringList &paths)
572 {
573     if (paths.isEmpty()) {
574         qWarning("QFileSystemWatcher::removePaths: list is empty");
575         return;
576     }
577     Q_D(QFileSystemWatcher);
578     QStringList p = paths;
579     if (d->native)
580         p = d->native->removePaths(p, &d->files, &d->directories);
581     if (d->poller)
582         p = d->poller->removePaths(p, &d->files, &d->directories);
583     if (d->forced)
584         p = d->forced->removePaths(p, &d->files, &d->directories);
585 }
586 
587 /*!
588     \fn void QFileSystemWatcher::fileChanged(const QString &path)
589 
590     This signal is emitted when the file at the specified \a path is
591     modified, renamed or removed from disk.
592 
593     \sa directoryChanged()
594 */
595 
596 /*!
597     \fn void QFileSystemWatcher::directoryChanged(const QString &path)
598 
599     This signal is emitted when the directory at a specified \a path,
600     is modified (e.g., when a file is added, modified or deleted) or
601     removed from disk. Note that if there are several changes during a
602     short period of time, some of the changes might not emit this
603     signal. However, the last change in the sequence of changes will
604     always generate this signal.
605 
606     \sa fileChanged()
607 */
608 
609 /*!
610     \fn QStringList QFileSystemWatcher::directories() const
611 
612     Returns a list of paths to directories that are being watched.
613 
614     \sa files()
615 */
616 
617 /*!
618     \fn QStringList QFileSystemWatcher::files() const
619 
620     Returns a list of paths to files that are being watched.
621 
622     \sa directories()
623 */
624 
directories() const625 QStringList QFileSystemWatcher::directories() const
626 {
627     Q_D(const QFileSystemWatcher);
628     return d->directories;
629 }
630 
files() const631 QStringList QFileSystemWatcher::files() const
632 {
633     Q_D(const QFileSystemWatcher);
634     return d->files;
635 }
636 
637 QT_END_NAMESPACE
638 
639 #include "moc_qfilesystemwatcher.cpp"
640 
641 #include "qfilesystemwatcher.moc"
642 
643 #endif // QT_NO_FILESYSTEMWATCHER
644 
645