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::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 | |
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 | 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 | |
105 | void 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 | |
118 | void 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 | |
133 | void 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 | |
150 | void 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 | |
164 | void 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 | */ |
227 | QFileSystemWatcher::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 | */ |
237 | QFileSystemWatcher::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 | */ |
247 | QFileSystemWatcher::~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 | */ |
273 | bool 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 | |
284 | static 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 | */ |
318 | QStringList 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 | */ |
372 | bool 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 | */ |
394 | QStringList 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 | |
458 | QStringList QFileSystemWatcher::directories() const |
459 | { |
460 | Q_D(const QFileSystemWatcher); |
461 | return d->directories; |
462 | } |
463 | |
464 | QStringList QFileSystemWatcher::files() const |
465 | { |
466 | Q_D(const QFileSystemWatcher); |
467 | return d->files; |
468 | } |
469 | |
470 | QT_END_NAMESPACE |
471 | |
472 | #include "moc_qfilesystemwatcher.cpp" |
473 | #include "moc_qfilesystemwatcher_p.cpp" |
474 | |
475 | |