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 | |
68 | QT_BEGIN_NAMESPACE |
69 | |
70 | Q_LOGGING_CATEGORY(lcWatcher, "qt.core.filesystemwatcher" ) |
71 | |
72 | QFileSystemWatcherEngine *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 | |
90 | QFileSystemWatcherPrivate::QFileSystemWatcherPrivate() |
91 | : native(nullptr), poller(nullptr) |
92 | { |
93 | } |
94 | |
95 | void 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 | |
122 | void 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 | |
139 | void 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 | |
152 | void 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 | |
167 | void 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 | |
184 | void 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 | |
198 | void 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 | */ |
261 | QFileSystemWatcher::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 | */ |
271 | QFileSystemWatcher::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 | */ |
281 | QFileSystemWatcher::~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 | */ |
307 | bool 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 | |
318 | static 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 | */ |
352 | QStringList 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 | */ |
406 | bool 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 | */ |
428 | QStringList 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 | |
492 | QStringList QFileSystemWatcher::directories() const |
493 | { |
494 | Q_D(const QFileSystemWatcher); |
495 | return d->directories; |
496 | } |
497 | |
498 | QStringList QFileSystemWatcher::files() const |
499 | { |
500 | Q_D(const QFileSystemWatcher); |
501 | return d->files; |
502 | } |
503 | |
504 | QT_END_NAMESPACE |
505 | |
506 | #include "moc_qfilesystemwatcher.cpp" |
507 | #include "moc_qfilesystemwatcher_p.cpp" |
508 | |
509 | |