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

source code of qtbase/src/corelib/io/qfilesystemwatcher.cpp