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