diff --git a/.gitignore b/.gitignore index 9e56d41..2e218ff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,11 @@ .Rhistory .RData .Ruserdata +*.Rcheck/ +*.tar.gz +src/*.o +src/*.so +src/*.dll /docs/ /revdep/ /.claude/ diff --git a/NEWS.md b/NEWS.md index 6bdd438..c5dddb1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # watcher (development version) -* Fixes installation failing on systems with a partial or broken system 'libfswatch', now falling back to the bundled build. +* Fixes installation failing on systems with a partial or broken system 'libfswatch', now falling back to the bundled build (#48). +* Updates bundled 'libfswatch' source to 1.22.0 release. On Linux, this fixes 'Renamed' events reporting only the watched directory path when a file is moved in from an unwatched location. # watcher 0.2.0 diff --git a/src/fswatch/README.md b/src/fswatch/README.md index b239c27..ccb9f84 100644 --- a/src/fswatch/README.md +++ b/src/fswatch/README.md @@ -1,3 +1,9 @@ +

+ + fswatch logo + +

+ [![License](https://img.shields.io/badge/license-GPL--3.0-blue.svg?style=flat)](https://github.com/emcrisostomo/fswatch/blob/master/COPYING) [![C/C++ CI](https://github.com/emcrisostomo/fswatch/actions/workflows/c-cpp.yml/badge.svg)](https://github.com/emcrisostomo/fswatch/actions/workflows/c-cpp.yml) [![CMake](https://github.com/emcrisostomo/fswatch/actions/workflows/cmake.yml/badge.svg)](https://github.com/emcrisostomo/fswatch/actions/workflows/cmake.yml) @@ -16,6 +22,8 @@ several monitors: and its derivatives. * A monitor based on _inotify_, a Linux kernel subsystem that reports file system changes to applications. + * A monitor based on _fanotify_, a Linux kernel subsystem that reports file + system events to applications. * A monitor based on _ReadDirectoryChangesW_, a Microsoft Windows API that reports changes to a directory. * A monitor which periodically stats the file system, saves file modification @@ -57,7 +65,8 @@ Features * Support for many OS-specific APIs such as kevent, inotify, and FSEvents. * Recursive directory monitoring. - * Path filtering using including and excluding regular expressions. + * Path filtering using including and excluding regular expressions, with + legacy and conjunctive evaluation modes. * Customizable record format. * Support for periodic idle events. @@ -80,11 +89,38 @@ The limitations of `fswatch` depend largely on the monitor being used: every file that cannot be opened. * The **inotify** monitor, available on Linux since kernel 2.6.13, may suffer - a queue overflow if events are generated faster than they are read from the - queue. In any case, the application is guaranteed to receive an overflow - notification which can be handled to gracefully recover. `fswatch` - currently throws an exception if a queue overflow occurs. Future versions - will handle the overflow by emitting proper notifications. + from several limitations. It provides no information about the user or + process that triggered an event, so it cannot easily distinguish events it + triggers itself from those triggered by other processes. It reports only + events triggered through the filesystem API, so it does not catch remote + events on network filesystems and cannot monitor pseudo-filesystems such as + `/proc`, `/sys`, or `/dev/pts`. It also does not report accesses or + modifications that occur through `mmap(2)`, `msync(2)`, or `munmap(2)`. + Events identify files by name, but the name may already have been deleted + or renamed by the time the application handles the event. Events are also + identified by watch descriptors, so applications must cache a mapping to + pathnames if they need one, and directory renames can affect several cached + pathnames at once. Inotify monitoring of directories is not recursive, so + additional watches must be created for subdirectories, and that can take a + significant amount of time on large trees. If a new subdirectory appears + or is renamed into a monitored tree, the application may need to rescan it + immediately after adding the watch so that it does not miss children that + already exist. The event queue can overflow, in which case events are lost + and robust applications may need to rebuild some or all of their cache. + Finally, if a filesystem is mounted on top of a monitored directory, no + event is generated until the filesystem is unmounted again. + + * The **fanotify** monitor, available on Linux since kernel 2.6.37, reports + only events triggered through the filesystem API, so it does not catch + remote events on network filesystems. It also does not report accesses or + modifications that occur through `mmap(2)`, `msync(2)`, or `munmap(2)`. + Directory events are generated only when the directory itself is opened, + read, and closed, so adding, removing, or changing children does not create + events for the monitored directory itself. Fanotify monitoring of + directories is not recursive: subdirectories require additional marks, and + using `FAN_CREATE` to detect them is racy because events may be lost before + the new mark is added. Monitoring mounts or entire filesystems avoids this + race. The event queue can also overflow, in which case events are lost. * The **Windows** monitor can only establish a watch _directories_, not files. To watch a file, its parent directory must be watched in order to receive @@ -215,7 +251,7 @@ Documentation * Texinfo documentation, included with the distribution. * HTML documentation. * PDF documentation. - * A [wiki] page. + * The [GitHub website]. * A man page. `fswatch` official documentation is provided in Texinfo format. This is the @@ -223,12 +259,15 @@ most comprehensive source of information about `fswatch` and the only authoritative one. The man page, in particular, is a stub that suggests the user to use the info page instead. +The project website also provides maintained online documentation and release +resources. + If you are installing `fswatch` using a package manager and you would like the PDF manual to be bundled into the package, please send a feature request to the package maintainer. -[documentation]: http://emcrisostomo.github.io/fswatch/doc -[wiki]: https://github.com/emcrisostomo/fswatch/wiki +[documentation]: https://emcrisostomo.github.io/fswatch/documentation.html +[GitHub website]: https://emcrisostomo.github.io/fswatch/ Localization ------------ @@ -315,7 +354,7 @@ v. 2.0. ----- -Copyright (c) 2013-2025 Enrico M. Crisostomo +Copyright (c) 2013-2026 Enrico M. Crisostomo This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/fanotify_monitor.cpp b/src/fswatch/libfswatch/src/libfswatch/c++/fanotify_monitor.cpp index 75e2ff9..cb1834b 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/fanotify_monitor.cpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/fanotify_monitor.cpp @@ -409,7 +409,7 @@ namespace fsw return true; } - void fanotify_monitor::scan(const std::filesystem::path& path, const bool accept_non_dirs) + void fanotify_monitor::scan(const std::filesystem::path& path, const bool is_root_path) { try { @@ -419,14 +419,14 @@ namespace fsw if (follow_symlinks && std::filesystem::is_symlink(status)) { - scan(std::filesystem::read_symlink(path), accept_non_dirs); + scan(std::filesystem::read_symlink(path), is_root_path); return; } const bool is_dir = std::filesystem::is_directory(status); - if (!is_dir && !accept_non_dirs) return; + if (should_prune_path(path.string(), is_dir, is_root_path)) return; + if (!is_dir && !is_root_path) return; if (!is_dir && directory_only) return; - if (!accept_path(path.string())) return; if (is_watched(path.string())) return; if (!add_mark(path)) return; if (!recursive || !is_dir) return; @@ -447,7 +447,7 @@ namespace fsw { for (const std::string& path : paths) { - if (!is_watched(path)) scan(path); + if (!is_watched(path)) scan(path, true); } } diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/fanotify_monitor.hpp b/src/fswatch/libfswatch/src/libfswatch/c++/fanotify_monitor.hpp index 54a58db..f30c630 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/fanotify_monitor.hpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/fanotify_monitor.hpp @@ -62,7 +62,7 @@ namespace fsw void initialize(); void scan_root_paths(); - void scan(const std::filesystem::path& path, bool accept_non_dirs = true); + void scan(const std::filesystem::path& path, bool is_root_path = false); bool add_mark(const std::filesystem::path& path); bool is_watched(const std::string& path) const; void process_pending_paths(); diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/fen_monitor.cpp b/src/fswatch/libfswatch/src/libfswatch/c++/fen_monitor.cpp index 06a3311..0278156 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/fen_monitor.cpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/fen_monitor.cpp @@ -275,9 +275,10 @@ namespace fsw } bool is_dir = S_ISDIR(fd_stat.st_mode); + if (should_prune_path(path.string(), is_dir, is_root_path)) return true; if (!is_dir && !is_root_path && directory_only) return true; - if (!is_dir && !accept_path(path)) return true; + if (!is_dir && !is_root_path && !accept_path(path)) return true; if (!is_dir) return add_watch(path, fd_stat); if (!recursive) return true; diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/inotify_monitor.cpp b/src/fswatch/libfswatch/src/libfswatch/c++/inotify_monitor.cpp index 7931464..9c073eb 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/inotify_monitor.cpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/inotify_monitor.cpp @@ -245,7 +245,7 @@ namespace fsw return (inotify_desc != -1); } - void inotify_monitor::scan(const std::filesystem::path& path, const bool accept_non_dirs) + void inotify_monitor::scan(const std::filesystem::path& path, const bool is_root_path) { try { @@ -260,24 +260,25 @@ namespace fsw if (follow_symlinks && std::filesystem::is_symlink(status)) { auto link_path = std::filesystem::read_symlink(path); - scan(link_path, accept_non_dirs); + scan(link_path, is_root_path); return; } const bool is_dir = std::filesystem::is_directory(status); + if (should_prune_path(path.string(), is_dir, is_root_path)) return; /* * When watching a directory the inotify API will return change events of * first-level children. Therefore, we do not need to manually add a watch - * for a child unless it is a directory. By default, accept_non_dirs is - * true to allow watching a file when first invoked on a node. + * for a child unless it is a directory. Directories and root paths must + * remain watchable so output filters do not prevent discovery of matching + * children. * * For the same reason, the directory_only flag is ignored and treated as if * it were always set to true. */ - if (!is_dir && !accept_non_dirs) return; + if (!is_dir && !is_root_path) return; if (!is_dir && directory_only) return; - if (!accept_path(path)) return; if (!add_watch(path)) return; if (!recursive || !is_dir) return; @@ -304,7 +305,7 @@ namespace fsw { for (const std::string& path : paths) { - if (!is_watched(path)) scan(path); + if (!is_watched(path)) scan(path, true); } } @@ -328,7 +329,17 @@ namespace fsw if (!flags.empty()) { - impl->events.emplace_back(impl->wd_to_path[event->wd], impl->curr_time, flags, event->cookie); + std::ostringstream filename_stream; + filename_stream << impl->wd_to_path[event->wd]; + + if ((event->mask & (IN_MOVED_FROM | IN_MOVED_TO)) && + event->len > 0 && event->name[0] != '\0') + { + filename_stream << "/"; + filename_stream << event->name; + } + + impl->events.emplace_back(filename_stream.str(), impl->curr_time, flags, event->cookie); } } diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/inotify_monitor.hpp b/src/fswatch/libfswatch/src/libfswatch/c++/inotify_monitor.hpp index b8ad385..ede9bd3 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/inotify_monitor.hpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/inotify_monitor.hpp @@ -82,7 +82,7 @@ namespace fsw void preprocess_dir_event(const struct inotify_event *event); void preprocess_event(const struct inotify_event *event); void preprocess_node_event(const struct inotify_event *event); - void scan(const std::filesystem::path& path, const bool accept_non_dirs = true); + void scan(const std::filesystem::path& path, bool is_root_path = false); bool add_watch(const std::string& path); void process_pending_events(); void process_synthetic_events(); diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/kqueue_monitor.cpp b/src/fswatch/libfswatch/src/libfswatch/c++/kqueue_monitor.cpp index 9d09f75..6280c94 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/kqueue_monitor.cpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/kqueue_monitor.cpp @@ -177,7 +177,7 @@ namespace fsw return true; } - void kqueue_monitor::scan(const std::filesystem::path& path) + void kqueue_monitor::scan(const std::filesystem::path& path, const bool is_root_path) { try { @@ -192,17 +192,18 @@ namespace fsw if (follow_symlinks && std::filesystem::is_symlink(status)) { const auto link_path = std::filesystem::read_symlink(path); - scan(link_path); + scan(link_path, is_root_path); return; } const bool is_dir = std::filesystem::is_directory(status); + if (should_prune_path(path.string(), is_dir, is_root_path)) return; // Do not fall through if the monitor is set to watch directories only, // except for the case of root paths, where the user explicitly asked to // watch the file itself. if (!is_dir && directory_only) return; - if (!accept_path(path)) return; + if (!is_root_path && !is_dir && !accept_path(path)) return; // TODO: C++17 doesn't provide a single, comparable, type to represent st_mode struct stat fd_stat; @@ -217,7 +218,7 @@ namespace fsw for (const auto& entry : entries) { - scan(entry); + scan(entry, false); } } catch (const std::filesystem::filesystem_error& e) @@ -270,7 +271,7 @@ namespace fsw { if (is_path_watched(path)) continue; - scan(path); + scan(path, true); } } diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/kqueue_monitor.hpp b/src/fswatch/libfswatch/src/libfswatch/c++/kqueue_monitor.hpp index bee5a18..27f0039 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/kqueue_monitor.hpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/kqueue_monitor.hpp @@ -77,7 +77,7 @@ namespace fsw void initialize_kqueue(); void terminate_kqueue(); - void scan(const std::filesystem::path& path); + void scan(const std::filesystem::path& path, bool is_root_path = false); bool add_watch(const std::string& path, const struct stat& fd_stat); bool is_path_watched(const std::string& path) const; void remove_deleted(); diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp b/src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp index 462bbef..28b75b0 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/monitor.cpp @@ -134,6 +134,27 @@ namespace fsw } } + void monitor::add_prune_filter(const monitor_filter& filter) + { + std::regex::flag_type regex_flags = std::regex::basic; + + if (filter.extended) regex_flags = std::regex::extended; + if (!filter.case_sensitive) regex_flags |= std::regex::icase; + + try + { + prune_filters.emplace_back(filter.text, regex_flags); + } + catch (const std::regex_error& error) + { + throw libfsw_exception( + string_utils::string_from_format( + _("An error occurred during the compilation of %s"), + filter.text.c_str()), + FSW_ERR_INVALID_REGEX); + } + } + void monitor::set_property(const std::string& name, const std::string& value) { properties[name] = value; @@ -151,12 +172,35 @@ namespace fsw void monitor::set_filters(const std::vector& filters) { + this->filters.clear(); + for (const monitor_filter& filter : filters) { add_filter(filter); } } + void monitor::set_prune_filters(const std::vector& filters) + { + prune_filters.clear(); + + for (const monitor_filter& filter : filters) + { + add_prune_filter(filter); + } + } + + void monitor::set_filter_mode(fsw_filter_mode mode) + { + if (mode != fsw_filter_mode::filter_mode_legacy && + mode != fsw_filter_mode::filter_mode_conjunctive) + { + throw libfsw_exception(_("Unknown filter mode."), FSW_ERR_UNKNOWN_VALUE); + } + + filter_mode = mode; + } + void monitor::set_follow_symlinks(bool follow) { follow_symlinks = follow; @@ -181,6 +225,29 @@ namespace fsw bool monitor::accept_path(const std::string& path) const { + if (filter_mode == fsw_filter_mode::filter_mode_conjunctive) + { + bool has_includes = false; + bool included = false; + + for (const auto& filter : filters) + { + const bool matches = std::regex_search(path, filter.regex); + + if (filter.type == fsw_filter_type::filter_include) + { + has_includes = true; + included = included || matches; + } + else if (matches) + { + return false; + } + } + + return !has_includes || included; + } + bool is_excluded = false; for (const auto& filter : filters) @@ -196,6 +263,18 @@ namespace fsw return !is_excluded; } + bool monitor::should_prune_path(const std::string& path, + bool is_dir, + bool is_root_path) const + { + if (!is_dir || is_root_path) return false; + + return std::any_of(prune_filters.begin(), + prune_filters.end(), + [&path](const std::regex& filter) + { return std::regex_search(path, filter); }); + } + void *monitor::get_context() const { return context; diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/monitor.hpp b/src/fswatch/libfswatch/src/libfswatch/c++/monitor.hpp index 570b811..564dea9 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/monitor.hpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/monitor.hpp @@ -36,6 +36,7 @@ # include # include # include +# include # include "event.hpp" # include "libfswatch/c/cmonitor.h" @@ -309,6 +310,42 @@ namespace fsw */ void set_filters(const std::vector& filters); + /** + * @brief Set the path filter evaluation mode. + * + * The default legacy mode accepts a path as soon as it matches an include + * filter, otherwise rejects it if it matches an exclude filter, otherwise + * accepts it. Conjunctive mode accepts paths that match at least one + * include filter, or any path if no include filters exist, and then rejects + * paths that match any exclude filter. + * + * @param mode The path filter evaluation mode. + */ + void set_filter_mode(fsw_filter_mode mode); + + /** + * @brief Add a traversal prune filter. + * + * This function adds a monitor_filter instance to the prune filter list. + * Prune filters are used by recursive monitors to decide whether a + * non-root directory should be skipped while traversing a hierarchy. They + * are not event filters: a pruned directory and its descendants are not + * scanned or monitored by implementations that use recursive traversal. + * + * @param filter The prune filter to add. + */ + void add_prune_filter(const monitor_filter& filter); + + /** + * @brief Set the traversal prune filters. + * + * This function sets the list of prune filters, substituting existing + * filters if any. + * + * @param filters The prune filters to set. + */ + void set_prune_filters(const std::vector& filters); + /** * @brief Follow symlinks. * @@ -458,6 +495,23 @@ namespace fsw */ bool accept_path(const std::string& path) const; + /** + * @brief Check whether a directory should be pruned from recursive scans. + * + * This function checks @p path against the prune filters of the monitor to + * determine whether recursive traversal should skip it. Root paths are + * never pruned by this helper so that explicit paths passed by the user + * remain eligible for monitor setup. + * + * @param path The path to check. + * @param is_dir @c true if the path is a directory. + * @param is_root_path @c true if the path is a monitor root. + * @return @c true if the directory should be pruned, @c false otherwise. + */ + bool should_prune_path(const std::string& path, + bool is_dir, + bool is_root_path) const; + /** * @brief Notify change events. * @@ -614,7 +668,9 @@ namespace fsw private: std::chrono::milliseconds get_latency_ms() const; std::vector filters; + std::vector prune_filters; std::vector event_type_filters; + fsw_filter_mode filter_mode = fsw_filter_mode::filter_mode_legacy; static void inactivity_callback(monitor *mon); mutable std::atomic last_notification; diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/poll_monitor.cpp b/src/fswatch/libfswatch/src/libfswatch/c++/poll_monitor.cpp index 24da8cd..11f268a 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/poll_monitor.cpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/poll_monitor.cpp @@ -120,7 +120,9 @@ namespace fsw return poll_callback(path, fd_stat); } - void poll_monitor::scan(const path& path, const path_visitor& fn) + void poll_monitor::scan(const path& path, + const path_visitor& fn, + const bool is_root_path) { try { @@ -135,19 +137,19 @@ namespace fsw if (follow_symlinks && std::filesystem::is_symlink(status)) { auto link_path = std::filesystem::read_symlink(path); - scan(link_path, fn); + scan(link_path, fn, is_root_path); return; } #if defined(_WIN32) && !defined(__CYGWIN__) fsw_log_perror("Poll monitor on Windows requires Cygwin"); #else - if (!accept_path(path)) return; - // TODO: C++17 doesn't standardize access to ctime, so we need to keep // using lstat for now. struct stat fd_stat; if (!stat_path(path, fd_stat, follow_symlinks)) return; + if (should_prune_path(path.string(), S_ISDIR(fd_stat.st_mode), is_root_path)) return; + if (!is_root_path && !S_ISDIR(fd_stat.st_mode) && !accept_path(path)) return; if (!add_path(path, fd_stat, fn)) return; if (!recursive) return; @@ -158,7 +160,7 @@ namespace fsw for (const auto& entry : entries) { - scan(entry.path(), fn); + scan(entry.path(), fn, false); } } catch (const std::filesystem::filesystem_error& e) @@ -195,7 +197,7 @@ namespace fsw for (const string& path : paths) { - scan(path, fn); + scan(path, fn, true); } find_removed_files(); @@ -211,7 +213,7 @@ namespace fsw for (const string& path : paths) { - scan(path, fn); + scan(path, fn, true); } } diff --git a/src/fswatch/libfswatch/src/libfswatch/c++/poll_monitor.hpp b/src/fswatch/libfswatch/src/libfswatch/c++/poll_monitor.hpp index f0e8135..4b13cf2 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c++/poll_monitor.hpp +++ b/src/fswatch/libfswatch/src/libfswatch/c++/poll_monitor.hpp @@ -77,7 +77,9 @@ namespace fsw struct poll_monitor_data; - void scan(const std::filesystem::path& path, const path_visitor& fn); + void scan(const std::filesystem::path& path, + const path_visitor& fn, + bool is_root_path = false); void collect_initial_data(); void collect_data(); bool add_path(const std::string& path, diff --git a/src/fswatch/libfswatch/src/libfswatch/c/cfilter.h b/src/fswatch/libfswatch/src/libfswatch/c/cfilter.h index cf7d08d..6fa25a1 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c/cfilter.h +++ b/src/fswatch/libfswatch/src/libfswatch/c/cfilter.h @@ -17,7 +17,7 @@ * @file * @brief Header of the `libfswatch` library functions for filter management. * - * @copyright Copyright (c) 2014-2016 Enrico M. Crisostomo + * @copyright Copyright (c) 2014-2026 Enrico M. Crisostomo * @license GNU General Public License v. 3.0 * @author Enrico M. Crisostomo * @version 1.8.0 @@ -41,6 +41,20 @@ extern "C" filter_exclude }; + /** + * @brief Path filter evaluation mode. + * + * filter_mode_legacy preserves the historical include-overrides-exclude + * behavior. filter_mode_conjunctive accepts paths that match at least one + * inclusion filter, or all paths when no inclusion filters exist, and then + * rejects paths matching any exclusion filter. + */ + enum fsw_filter_mode + { + filter_mode_legacy, + filter_mode_conjunctive + }; + typedef struct fsw_cmonitor_filter { char * text; diff --git a/src/fswatch/libfswatch/src/libfswatch/c/libfswatch.cpp b/src/fswatch/libfswatch/src/libfswatch/c/libfswatch.cpp index ca6b2e8..5e720f2 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c/libfswatch.cpp +++ b/src/fswatch/libfswatch/src/libfswatch/c/libfswatch.cpp @@ -195,6 +195,7 @@ * active_monitor->set_directory_only(directory_only); * active_monitor->set_event_type_filters(event_filters); * active_monitor->set_filters(filters); + * active_monitor->set_prune_filters(prune_filters); * active_monitor->set_follow_symlinks(follow_symlinks); * active_monitor->set_watch_access(watch_access); * @@ -416,7 +417,8 @@ * A _path filter_ (fsw::monitor_filter) can be used to filter event paths. A * filter type (::fsw_filter_type) determines whether the filter regular * expression is used to include and exclude paths from the list of the events - * processed by the library. `libfswatch` processes filters this way: + * processed by the library. By default, `libfswatch` uses the legacy filter + * mode and processes filters this way: * * - If a path matches an including filter, the path is _accepted_ no matter * any other filter. @@ -433,6 +435,35 @@ * - Inclusion filters may override any other exclusion filter. * * - The order in the filter definition has no effect. + * + * The filter mode can be set to ::filter_mode_conjunctive to evaluate filters + * as a conjunction: + * + * - If no inclusion filters are specified, all paths are candidates. + * + * - If at least one inclusion filter is specified, a path must match at + * least one inclusion filter to be a candidate. + * + * - Any matching exclusion filter rejects the path. + * + * Said another way, conjunctive mode accepts paths matching: + * + * `(no includes OR matches include) AND NOT matches exclude` + * + * @section path-pruning Recursive Traversal Pruning + * + * Prune filters are separate from path filters. A prune filter is a regular + * expression used by recursive monitors to decide whether traversal should + * descend into a non-root directory. When a directory is pruned, that + * directory's descendants are not scanned or monitored by implementations that + * traverse directory trees. + * + * Pruning is applied before normal event filtering and is intentionally + * opt-in: unlike exclusion filters, prune filters can make included + * descendants impossible to observe. + * + * Root paths are not pruned, even if they match a prune filter, because those + * paths were explicitly supplied by the caller. */ #include "libfswatch/gettext_defs.h" #include @@ -465,7 +496,9 @@ using FSW_SESSION = struct FSW_SESSION bool recursive; bool directory_only; bool follow_symlinks; + fsw_filter_mode filter_mode; vector filters; + vector prune_filters; vector event_type_filters; map properties; void *data; @@ -806,6 +839,31 @@ FSW_STATUS fsw_add_filter(const FSW_HANDLE handle, return fsw_set_last_error(FSW_OK); } +FSW_STATUS fsw_set_filter_mode(const FSW_HANDLE handle, + const enum fsw_filter_mode mode) +{ + if (mode != fsw_filter_mode::filter_mode_legacy && + mode != fsw_filter_mode::filter_mode_conjunctive) + { + return fsw_set_last_error(FSW_ERR_UNKNOWN_VALUE); + } + + FSW_SESSION *session = get_session(handle); + session->filter_mode = mode; + + return fsw_set_last_error(FSW_OK); +} + +FSW_STATUS fsw_add_prune_filter(const FSW_HANDLE handle, + const fsw_cmonitor_filter filter) +{ + FSW_SESSION *session = get_session(handle); + session->prune_filters.push_back( + {filter.text, filter.type, filter.case_sensitive, filter.extended}); + + return fsw_set_last_error(FSW_OK); +} + bool fsw_is_running(const FSW_HANDLE handle) { FSW_SESSION *session = get_session(handle); @@ -837,7 +895,9 @@ FSW_STATUS fsw_start_monitor(const FSW_HANDLE handle) return fsw_set_last_error(int(FSW_ERR_MONITOR_ALREADY_RUNNING)); session->monitor->set_allow_overflow(session->allow_overflow); + session->monitor->set_filter_mode(session->filter_mode); session->monitor->set_filters(session->filters); + session->monitor->set_prune_filters(session->prune_filters); session->monitor->set_event_type_filters(session->event_type_filters); session->monitor->set_follow_symlinks(session->follow_symlinks); if (session->latency) session->monitor->set_latency(session->latency); diff --git a/src/fswatch/libfswatch/src/libfswatch/c/libfswatch.h b/src/fswatch/libfswatch/src/libfswatch/c/libfswatch.h index 179d7b4..07b4a2a 100644 --- a/src/fswatch/libfswatch/src/libfswatch/c/libfswatch.h +++ b/src/fswatch/libfswatch/src/libfswatch/c/libfswatch.h @@ -182,6 +182,28 @@ extern "C" FSW_STATUS fsw_add_filter(const FSW_HANDLE handle, const fsw_cmonitor_filter filter); + /** + * Sets the path filter evaluation mode. By default, libfswatch uses legacy + * include-overrides-exclude semantics. See cfilter.h for the definition of + * fsw_filter_mode. + */ + FSW_STATUS fsw_set_filter_mode(const FSW_HANDLE handle, + const enum fsw_filter_mode mode); + + /** + * Adds a prune filter to the current session. A prune filter is a regular + * expression that prevents recursive traversal from descending into matching + * non-root directories. It is not an event filter: pruned directories and + * their descendants are not scanned or monitored by recursive monitors that + * traverse directory trees. + * + * See cfilter.h for the definition of fsw_cmonitor_filter. The filter type + * is ignored, while the expression, case-sensitivity, and extended-regex + * flags are honoured. + */ + FSW_STATUS fsw_add_prune_filter(const FSW_HANDLE handle, + const fsw_cmonitor_filter filter); + /** * Starts the monitor if it is properly configured. Depending on the type of * monitor this call might return when a monitor is stopped or not. diff --git a/tests/testthat/test-watch.R b/tests/testthat/test-watch.R index c18d348..b769925 100644 --- a/tests/testthat/test-watch.R +++ b/tests/testthat/test-watch.R @@ -76,6 +76,41 @@ test_that("watcher() callbacks", { rm(w) }) +test_that("watcher() reports full path for one-sided move into watched dir", { + skip_if(R.version$arch == "aarch64" && !Sys.getenv("NOT_CRAN") == "true") + # Regression test for libfswatch issue #387: pre-1.22.0 the Linux inotify + # monitor dropped the child filename from one-sided move events, reporting + # only the watched directory path. + staging <- file.path(tempdir(), "watcher-staging") + dir.create(staging) + src <- file.path(staging, "moved-in") + dest <- file.path(dir, "moved-in") + paths <- character() + w <- watcher( + dir, + callback = ~ { + paths <<- c(paths, .x) + }, + latency = 0.2 + ) + expect_true(w$start()) + Sys.sleep(1) + file.create(src) + file.rename(src, dest) + for (i in 1:20) { + later::run_now(0.5) + if (any(normalizePath(paths, mustWork = FALSE) == normalizePath(dest))) { + break + } + } + expect_true(any( + normalizePath(paths, mustWork = FALSE) == normalizePath(dest) + )) + expect_true(w$stop()) + rm(w) + unlink(staging, recursive = TRUE, force = TRUE) +}) + unlink(dir, recursive = TRUE, force = TRUE) unlink(dir2, recursive = TRUE, force = TRUE) diff --git a/tools/update_libfswatch.sh b/tools/update_libfswatch.sh index ac88e74..b6055a8 100755 --- a/tools/update_libfswatch.sh +++ b/tools/update_libfswatch.sh @@ -318,6 +318,7 @@ echo "Cleaning up unnecessary files..." rm -rf "${STAGING_DIR}/libfswatch/doc" find "${STAGING_DIR}" -name "Makefile.am" -delete rm -f "${STAGING_DIR}/libfswatch/CMakeLists.txt" +rm -rf "${STAGING_DIR}/libfswatch/cmake" rm -f "${STAGING_DIR}/libfswatch/src/libfswatch.pc.in" rm -f "${STAGING_DIR}/libfswatch/libfswatch_config.in"