From 105d57cbddba75e178a8e5b3dd02223d0754b83c Mon Sep 17 00:00:00 2001
From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com>
Date: Fri, 25 Sep 2026 12:02:12 +0100
Subject: [PATCH] Update bundled libfswatch to 1.22.0
- Re-vendor from upstream 1.22.0 release; local patches re-applied and
verified, object lists/Makevars regenerated with no drift.
- Linux inotify: one-sided moves and paired renames now report the full
child path (upstream PR 388, issue 387).
- Add regression test for one-sided moves into a watched directory
(verified failing on the 1.20.1 bundle, passing on 1.22.0, Linux
aarch64 container).
- update_libfswatch.sh: also exclude upstream cmake/ package-config dir.
- gitignore build artefacts (*.Rcheck/, *.tar.gz, src/*.o, src/*.so,
src/*.dll).
---
.gitignore | 5 ++
NEWS.md | 3 +-
src/fswatch/README.md | 59 +++++++++++---
.../src/libfswatch/c++/fanotify_monitor.cpp | 10 +--
.../src/libfswatch/c++/fanotify_monitor.hpp | 2 +-
.../src/libfswatch/c++/fen_monitor.cpp | 3 +-
.../src/libfswatch/c++/inotify_monitor.cpp | 27 +++++--
.../src/libfswatch/c++/inotify_monitor.hpp | 2 +-
.../src/libfswatch/c++/kqueue_monitor.cpp | 11 +--
.../src/libfswatch/c++/kqueue_monitor.hpp | 2 +-
.../libfswatch/src/libfswatch/c++/monitor.cpp | 79 +++++++++++++++++++
.../libfswatch/src/libfswatch/c++/monitor.hpp | 56 +++++++++++++
.../src/libfswatch/c++/poll_monitor.cpp | 16 ++--
.../src/libfswatch/c++/poll_monitor.hpp | 4 +-
.../libfswatch/src/libfswatch/c/cfilter.h | 16 +++-
.../src/libfswatch/c/libfswatch.cpp | 62 ++++++++++++++-
.../libfswatch/src/libfswatch/c/libfswatch.h | 22 ++++++
tests/testthat/test-watch.R | 35 ++++++++
tools/update_libfswatch.sh | 1 +
19 files changed, 372 insertions(+), 43 deletions(-)
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 @@
+
+
+
+
+
+
[](https://github.com/emcrisostomo/fswatch/blob/master/COPYING)
[](https://github.com/emcrisostomo/fswatch/actions/workflows/c-cpp.yml)
[](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