1/****************************************************************************
2**
3** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
4** Contact: http://www.qt-project.org/legal
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 Digia. For licensing terms and
14** conditions see http://qt.digia.com/licensing. For further information
15** use the contact form at http://qt.digia.com/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 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 2.1 requirements
23** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24**
25** In addition, as a special exception, Digia gives you certain additional
26** rights. These rights are described in the Digia Qt LGPL Exception
27** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
28**
29** GNU General Public License Usage
30** Alternatively, this file may be used under the terms of the GNU
31** General Public License version 3.0 as published by the Free Software
32** Foundation and appearing in the file LICENSE.GPL included in the
33** packaging of this file. Please review the following information to
34** ensure the GNU General Public License version 3.0 requirements will be
35** met: http://www.gnu.org/copyleft/gpl.html.
36**
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
71QT_BEGIN_NAMESPACE
72
73enum { PollingInterval = 1000 };
74
75class 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:
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 }
98 FileInfo &operator=(const QFileInfo &fileInfo)
99 {
100 *this = FileInfo(fileInfo);
101 return *this;
102 }
103
104 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
118public:
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
128private Q_SLOTS:
129 void timeout();
130};
131
132QPollingFileSystemWatcherEngine::QPollingFileSystemWatcherEngine()
133{
134#ifndef QT_NO_THREAD
135 moveToThread(this);
136#endif
137}
138
139void QPollingFileSystemWatcherEngine::run()
140{
141 QTimer timer;
142 connect(&timer, SIGNAL(timeout()), SLOT(timeout()));
143 timer.start(PollingInterval);
144 (void) exec();
145}
146
147QStringList 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
176QStringList 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
201void QPollingFileSystemWatcherEngine::stop()
202{
203 quit();
204}
205
206void 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
249QFileSystemWatcherEngine *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
274QFileSystemWatcherPrivate::QFileSystemWatcherPrivate()
275 : native(0), poller(0), forced(0)
276{
277}
278
279void 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
295void 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
324void 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
341void 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
353void 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*/
420QFileSystemWatcher::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*/
430QFileSystemWatcher::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*/
440QFileSystemWatcher::~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*/
480void 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*/
507void 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*/
557void 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*/
571void 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
625QStringList QFileSystemWatcher::directories() const
626{
627 Q_D(const QFileSystemWatcher);
628 return d->directories;
629}
630
631QStringList QFileSystemWatcher::files() const
632{
633 Q_D(const QFileSystemWatcher);
634 return d->files;
635}
636
637QT_END_NAMESPACE
638
639#include "moc_qfilesystemwatcher.cpp"
640
641#include "qfilesystemwatcher.moc"
642
643#endif // QT_NO_FILESYSTEMWATCHER
644
645