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 "qfilesystemwatcher.h"
41#include "qfilesystemwatcher_p.h"
42
43#include <qdatetime.h>
44#include <qdir.h>
45#include <qfileinfo.h>
46#include <qloggingcategory.h>
47#include <qset.h>
48#include <qtimer.h>
49
50#if (defined(Q_OS_LINUX) || defined(Q_OS_QNX)) && QT_CONFIG(inotify)
51#define USE_INOTIFY
52#endif
53
54#include "qfilesystemwatcher_polling_p.h"
55#if defined(Q_OS_WIN)
56# include "qfilesystemwatcher_win_p.h"
57#elif defined(USE_INOTIFY)
58# include "qfilesystemwatcher_inotify_p.h"
59#elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD) || defined(QT_PLATFORM_UIKIT)
60# include "qfilesystemwatcher_kqueue_p.h"
61#elif defined(Q_OS_MACOS)
62# include "qfilesystemwatcher_fsevents_p.h"
63#endif
64
65#include <algorithm>
66#include <iterator>
67
68QT_BEGIN_NAMESPACE
69
70Q_LOGGING_CATEGORY(lcWatcher, "qt.core.filesystemwatcher")
71
72QFileSystemWatcherEngine *QFileSystemWatcherPrivate::createNativeEngine(QObject *parent)
73{
74#if defined(Q_OS_WIN)
75 return new QWindowsFileSystemWatcherEngine(parent);
76#elif defined(USE_INOTIFY)
77 // there is a chance that inotify may fail on Linux pre-2.6.13 (August
78 // 2005), so we can't just new inotify directly.
79 return QInotifyFileSystemWatcherEngine::create(parent);
80#elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) || defined(Q_OS_OPENBSD) || defined(QT_PLATFORM_UIKIT)
81 return QKqueueFileSystemWatcherEngine::create(parent);
82#elif defined(Q_OS_MACOS)
83 return QFseventsFileSystemWatcherEngine::create(parent);
84#else
85 Q_UNUSED(parent);
86 return 0;
87#endif
88}
89
90QFileSystemWatcherPrivate::QFileSystemWatcherPrivate()
91 : native(nullptr), poller(nullptr)
92{
93}
94
95void QFileSystemWatcherPrivate::init()
96{
97 Q_Q(QFileSystemWatcher);
98 native = createNativeEngine(q);
99 if (native) {
100 QObject::connect(native,
101 SIGNAL(fileChanged(QString,bool)),
102 q,
103 SLOT(_q_fileChanged(QString,bool)));
104 QObject::connect(native,
105 SIGNAL(directoryChanged(QString,bool)),
106 q,
107 SLOT(_q_directoryChanged(QString,bool)));
108#if defined(Q_OS_WIN)
109 QObject::connect(static_cast<QWindowsFileSystemWatcherEngine *>(native),
110 &QWindowsFileSystemWatcherEngine::driveLockForRemoval,
111 q, [this] (const QString &p) { _q_winDriveLockForRemoval(p); });
112 QObject::connect(static_cast<QWindowsFileSystemWatcherEngine *>(native),
113 &QWindowsFileSystemWatcherEngine::driveLockForRemovalFailed,
114 q, [this] (const QString &p) { _q_winDriveLockForRemovalFailed(p); });
115 QObject::connect(static_cast<QWindowsFileSystemWatcherEngine *>(native),
116 &QWindowsFileSystemWatcherEngine::driveRemoved,
117 q, [this] (const QString &p) { _q_winDriveRemoved(p); });
118#endif // Q_OS_WIN
119 }
120}
121
122void QFileSystemWatcherPrivate::initPollerEngine()
123{
124 if(poller)
125 return;
126
127 Q_Q(QFileSystemWatcher);
128 poller = new QPollingFileSystemWatcherEngine(q); // that was a mouthful
129 QObject::connect(poller,
130 SIGNAL(fileChanged(QString,bool)),
131 q,
132 SLOT(_q_fileChanged(QString,bool)));
133 QObject::connect(poller,
134 SIGNAL(directoryChanged(QString,bool)),
135 q,
136 SLOT(_q_directoryChanged(QString,bool)));
137}
138
139void QFileSystemWatcherPrivate::_q_fileChanged(const QString &path, bool removed)
140{
141 Q_Q(QFileSystemWatcher);
142 qCDebug(lcWatcher) << "file changed" << path << "removed?" << removed << "watching?" << files.contains(path);
143 if (!files.contains(path)) {
144 // the path was removed after a change was detected, but before we delivered the signal
145 return;
146 }
147 if (removed)
148 files.removeAll(path);
149 emit q->fileChanged(path, QFileSystemWatcher::QPrivateSignal());
150}
151
152void QFileSystemWatcherPrivate::_q_directoryChanged(const QString &path, bool removed)
153{
154 Q_Q(QFileSystemWatcher);
155 qCDebug(lcWatcher) << "directory changed" << path << "removed?" << removed << "watching?" << directories.contains(path);
156 if (!directories.contains(path)) {
157 // perhaps the path was removed after a change was detected, but before we delivered the signal
158 return;
159 }
160 if (removed)
161 directories.removeAll(path);
162 emit q->directoryChanged(path, QFileSystemWatcher::QPrivateSignal());
163}
164
165#if defined(Q_OS_WIN)
166
167void QFileSystemWatcherPrivate::_q_winDriveLockForRemoval(const QString &path)
168{
169 // Windows: Request to lock a (removable/USB) drive for removal, release
170 // its paths under watch, temporarily storing them should the lock fail.
171 Q_Q(QFileSystemWatcher);
172 QStringList pathsToBeRemoved;
173 auto pred = [&path] (const QString &f) { return !f.startsWith(path, Qt::CaseInsensitive); };
174 std::remove_copy_if(files.cbegin(), files.cend(),
175 std::back_inserter(pathsToBeRemoved), pred);
176 std::remove_copy_if(directories.cbegin(), directories.cend(),
177 std::back_inserter(pathsToBeRemoved), pred);
178 if (!pathsToBeRemoved.isEmpty()) {
179 q->removePaths(pathsToBeRemoved);
180 temporarilyRemovedPaths.insert(path.at(0), pathsToBeRemoved);
181 }
182}
183
184void QFileSystemWatcherPrivate::_q_winDriveLockForRemovalFailed(const QString &path)
185{
186 // Windows: Request to lock a (removable/USB) drive failed (blocked by other
187 // application), restore the watched paths.
188 Q_Q(QFileSystemWatcher);
189 if (!path.isEmpty()) {
190 const auto it = temporarilyRemovedPaths.find(path.at(0));
191 if (it != temporarilyRemovedPaths.end()) {
192 q->addPaths(it.value());
193 temporarilyRemovedPaths.erase(it);
194 }
195 }
196}
197
198void QFileSystemWatcherPrivate::_q_winDriveRemoved(const QString &path)
199{
200 // Windows: Drive finally removed, clear out paths stored in lock request.
201 if (!path.isEmpty())
202 temporarilyRemovedPaths.remove(path.at(0));
203}
204#endif // Q_OS_WIN
205
206/*!
207 \class QFileSystemWatcher
208 \inmodule QtCore
209 \brief The QFileSystemWatcher class provides an interface for monitoring files and directories for modifications.
210 \ingroup io
211 \since 4.2
212 \reentrant
213
214 QFileSystemWatcher monitors the file system for changes to files
215 and directories by watching a list of specified paths.
216
217 Call addPath() to watch a particular file or directory. Multiple
218 paths can be added using the addPaths() function. Existing paths can
219 be removed by using the removePath() and removePaths() functions.
220
221 QFileSystemWatcher examines each path added to it. Files that have
222 been added to the QFileSystemWatcher can be accessed using the
223 files() function, and directories using the directories() function.
224
225 The fileChanged() signal is emitted when a file has been modified,
226 renamed or removed from disk. Similarly, the directoryChanged()
227 signal is emitted when a directory or its contents is modified or
228 removed. Note that QFileSystemWatcher stops monitoring files once
229 they have been renamed or removed from disk, and directories once
230 they have been removed from disk.
231
232 \list
233 \li \b Notes:
234 \list
235 \li On systems running a Linux kernel without inotify support,
236 file systems that contain watched paths cannot be unmounted.
237
238 \li The act of monitoring files and directories for
239 modifications consumes system resources. This implies there is a
240 limit to the number of files and directories your process can
241 monitor simultaneously. On all BSD variants, for
242 example, an open file descriptor is required for each monitored
243 file. Some system limits the number of open file descriptors to 256
244 by default. This means that addPath() and addPaths() will fail if
245 your process tries to add more than 256 files or directories to
246 the file system monitor. Also note that your process may have
247 other file descriptors open in addition to the ones for files
248 being monitored, and these other open descriptors also count in
249 the total. \macos uses a different backend and does not
250 suffer from this issue.
251 \endlist
252 \endlist
253
254 \sa QFile, QDir
255*/
256
257
258/*!
259 Constructs a new file system watcher object with the given \a parent.
260*/
261QFileSystemWatcher::QFileSystemWatcher(QObject *parent)
262 : QObject(*new QFileSystemWatcherPrivate, parent)
263{
264 d_func()->init();
265}
266
267/*!
268 Constructs a new file system watcher object with the given \a parent
269 which monitors the specified \a paths list.
270*/
271QFileSystemWatcher::QFileSystemWatcher(const QStringList &paths, QObject *parent)
272 : QObject(*new QFileSystemWatcherPrivate, parent)
273{
274 d_func()->init();
275 addPaths(paths);
276}
277
278/*!
279 Destroys the file system watcher.
280*/
281QFileSystemWatcher::~QFileSystemWatcher()
282{ }
283
284/*!
285 Adds \a path to the file system watcher if \a path exists. The
286 path is not added if it does not exist, or if it is already being
287 monitored by the file system watcher.
288
289 If \a path specifies a directory, the directoryChanged() signal
290 will be emitted when \a path is modified or removed from disk;
291 otherwise the fileChanged() signal is emitted when \a path is
292 modified, renamed or removed.
293
294 If the watch was successful, true is returned.
295
296 Reasons for a watch failure are generally system-dependent, but
297 may include the resource not existing, access failures, or the
298 total watch count limit, if the platform has one.
299
300 \note There may be a system dependent limit to the number of
301 files and directories that can be monitored simultaneously.
302 If this limit is been reached, \a path will not be monitored,
303 and false is returned.
304
305 \sa addPaths(), removePath()
306*/
307bool QFileSystemWatcher::addPath(const QString &path)
308{
309 if (path.isEmpty()) {
310 qWarning("QFileSystemWatcher::addPath: path is empty");
311 return true;
312 }
313
314 QStringList paths = addPaths(QStringList(path));
315 return paths.isEmpty();
316}
317
318static QStringList empty_paths_pruned(const QStringList &paths)
319{
320 QStringList p;
321 p.reserve(paths.size());
322 const auto isEmpty = [](const QString &s) { return s.isEmpty(); };
323 std::remove_copy_if(paths.begin(), paths.end(),
324 std::back_inserter(p),
325 isEmpty);
326 return p;
327}
328
329/*!
330 Adds each path in \a paths to the file system watcher. Paths are
331 not added if they not exist, or if they are already being
332 monitored by the file system watcher.
333
334 If a path specifies a directory, the directoryChanged() signal
335 will be emitted when the path is modified or removed from disk;
336 otherwise the fileChanged() signal is emitted when the path is
337 modified, renamed, or removed.
338
339 The return value is a list of paths that could not be watched.
340
341 Reasons for a watch failure are generally system-dependent, but
342 may include the resource not existing, access failures, or the
343 total watch count limit, if the platform has one.
344
345 \note There may be a system dependent limit to the number of
346 files and directories that can be monitored simultaneously.
347 If this limit has been reached, the excess \a paths will not
348 be monitored, and they will be added to the returned QStringList.
349
350 \sa addPath(), removePaths()
351*/
352QStringList QFileSystemWatcher::addPaths(const QStringList &paths)
353{
354 Q_D(QFileSystemWatcher);
355
356 QStringList p = empty_paths_pruned(paths);
357
358 if (p.isEmpty()) {
359 qWarning("QFileSystemWatcher::addPaths: list is empty");
360 return p;
361 }
362 qCDebug(lcWatcher) << "adding" << paths;
363 const auto selectEngine = [this, d]() -> QFileSystemWatcherEngine* {
364#ifdef QT_BUILD_INTERNAL
365 const QString on = objectName();
366
367 if (Q_UNLIKELY(on.startsWith(QLatin1String("_qt_autotest_force_engine_")))) {
368 // Autotest override case - use the explicitly selected engine only
369 const auto forceName = QStringView{on}.mid(26);
370 if (forceName == QLatin1String("poller")) {
371 qCDebug(lcWatcher, "QFileSystemWatcher: skipping native engine, using only polling engine");
372 d_func()->initPollerEngine();
373 return d->poller;
374 } else if (forceName == QLatin1String("native")) {
375 qCDebug(lcWatcher, "QFileSystemWatcher: skipping polling engine, using only native engine");
376 return d->native;
377 }
378 return nullptr;
379 }
380#endif
381 // Normal runtime case - search intelligently for best engine
382 if(d->native) {
383 return d->native;
384 } else {
385 d_func()->initPollerEngine();
386 return d->poller;
387 }
388 };
389
390 if (auto engine = selectEngine())
391 p = engine->addPaths(p, &d->files, &d->directories);
392
393 return p;
394}
395
396/*!
397 Removes the specified \a path from the file system watcher.
398
399 If the watch is successfully removed, true is returned.
400
401 Reasons for watch removal failing are generally system-dependent,
402 but may be due to the path having already been deleted, for example.
403
404 \sa removePaths(), addPath()
405*/
406bool QFileSystemWatcher::removePath(const QString &path)
407{
408 if (path.isEmpty()) {
409 qWarning("QFileSystemWatcher::removePath: path is empty");
410 return true;
411 }
412
413 QStringList paths = removePaths(QStringList(path));
414 return paths.isEmpty();
415}
416
417/*!
418 Removes the specified \a paths from the file system watcher.
419
420 The return value is a list of paths which were not able to be
421 unwatched successfully.
422
423 Reasons for watch removal failing are generally system-dependent,
424 but may be due to the path having already been deleted, for example.
425
426 \sa removePath(), addPaths()
427*/
428QStringList QFileSystemWatcher::removePaths(const QStringList &paths)
429{
430 Q_D(QFileSystemWatcher);
431
432 QStringList p = empty_paths_pruned(paths);
433
434 if (p.isEmpty()) {
435 qWarning("QFileSystemWatcher::removePaths: list is empty");
436 return p;
437 }
438 qCDebug(lcWatcher) << "removing" << paths;
439
440 if (d->native)
441 p = d->native->removePaths(p, &d->files, &d->directories);
442 if (d->poller)
443 p = d->poller->removePaths(p, &d->files, &d->directories);
444
445 return p;
446}
447
448/*!
449 \fn void QFileSystemWatcher::fileChanged(const QString &path)
450
451 This signal is emitted when the file at the specified \a path is
452 modified, renamed or removed from disk.
453
454 \note As a safety measure, many applications save an open file by
455 writing a new file and then deleting the old one. In your slot
456 function, you can check \c watcher.files().contains(path).
457 If it returns \c false, check whether the file still exists
458 and then call \c addPath() to continue watching it.
459
460 \sa directoryChanged()
461*/
462
463/*!
464 \fn void QFileSystemWatcher::directoryChanged(const QString &path)
465
466 This signal is emitted when the directory at a specified \a path
467 is modified (e.g., when a file is added or deleted) or removed
468 from disk. Note that if there are several changes during a short
469 period of time, some of the changes might not emit this signal.
470 However, the last change in the sequence of changes will always
471 generate this signal.
472
473 \sa fileChanged()
474*/
475
476/*!
477 \fn QStringList QFileSystemWatcher::directories() const
478
479 Returns a list of paths to directories that are being watched.
480
481 \sa files()
482*/
483
484/*!
485 \fn QStringList QFileSystemWatcher::files() const
486
487 Returns a list of paths to files that are being watched.
488
489 \sa directories()
490*/
491
492QStringList QFileSystemWatcher::directories() const
493{
494 Q_D(const QFileSystemWatcher);
495 return d->directories;
496}
497
498QStringList QFileSystemWatcher::files() const
499{
500 Q_D(const QFileSystemWatcher);
501 return d->files;
502}
503
504QT_END_NAMESPACE
505
506#include "moc_qfilesystemwatcher.cpp"
507#include "moc_qfilesystemwatcher_p.cpp"
508
509