From f8d9956ea082e059b8d7ae1e4bd13456c4833bd1 Mon Sep 17 00:00:00 2001 From: Nathanael Ruf Date: Tue, 20 May 2025 09:07:27 +0200 Subject: [PATCH 01/13] Add IN_CLOEXEC, IN_NONBLOCK --- include/wtr/watcher.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index c7c756f0..9c454e00 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -1412,7 +1412,7 @@ inline auto make_sysres = []( auto make_inotify = [](result* ok) -> int { if (*ok >= result::e) return -1; - int in_fd = inotify_init(); + int in_fd = inotify_init(IN_CLOEXEC | IN_NONBLOCK); if (in_fd < 0) *ok = result::e_sys_api_inotify; return in_fd; }; From 2a0a98e1e07f5323bfdc12b083e46a1ae3c0f439 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 20 May 2025 10:38:40 +0200 Subject: [PATCH 02/13] Optional ignored paths argument Co-authored-by: Nathanael Ruf --- .../src/wtr/test_watcher/test_ignore_dir.cpp | 88 +++++++++++++++++++ include/wtr/watcher.hpp | 88 ++++++++++++++----- watcher-nodejs/lib/watcher-napi.cpp | 51 +++++++++-- watcher-nodejs/lib/watcher.ts | 8 +- 4 files changed, 203 insertions(+), 32 deletions(-) create mode 100644 devel/src/wtr/test_watcher/test_ignore_dir.cpp diff --git a/devel/src/wtr/test_watcher/test_ignore_dir.cpp b/devel/src/wtr/test_watcher/test_ignore_dir.cpp new file mode 100644 index 00000000..d8bd5545 --- /dev/null +++ b/devel/src/wtr/test_watcher/test_ignore_dir.cpp @@ -0,0 +1,88 @@ +#include "snitch/snitch.hpp" +#include "test_watcher/test_watcher.hpp" +#include "wtr/watcher.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +/* Test that should_skip ignores specified directories */ + +TEST_CASE("Ignored Paths", "[dir][ignore][filter]") +{ + namespace fs = std::filesystem; + using namespace std::chrono_literals; + using namespace wtr::watcher; + using namespace wtr::test_watcher; + + auto const tmpdir = make_local_tmp_dir(); + auto const ignored_dir = tmpdir / "ignored"; + auto const watched_dir = tmpdir / "watched"; + auto event_recv_list = std::vector{}; + auto event_recv_list_mtx = std::mutex{}; + auto event_sent_list = std::vector{}; + + REQUIRE(fs::exists(tmpdir) || fs::create_directory(tmpdir)); + REQUIRE(fs::create_directory(ignored_dir)); + REQUIRE(fs::create_directory(watched_dir)); + + std::this_thread::sleep_for(10ms); + + event_sent_list.push_back( + {std::string("s/self/live@").append(tmpdir.string()), + event::effect_type::create, + event::path_type::watcher}); + + auto watcher = watch( + tmpdir, + [&event_recv_list_mtx, &event_recv_list](event const& ev) { + auto _ = std::scoped_lock{event_recv_list_mtx}; + event_recv_list.push_back(ev); + }, + {ignored_dir.string()}); + + std::this_thread::sleep_for(100ms); + + // Create a file in the ignored directory + auto ignored_file = ignored_dir / "file.txt"; + std::ofstream(ignored_file).close(); + REQUIRE(fs::is_regular_file(ignored_file)); + + // Create a file in the watched directory + auto watched_file = watched_dir / "file.txt"; + std::ofstream(watched_file).close(); + REQUIRE(fs::is_regular_file(watched_file)); + + event_sent_list.push_back( + {watched_file, event::effect_type::create, event::path_type::file}); + + // Wait for events + for (int i = 0;; i++) { + std::this_thread::sleep_for(10ms); + auto _ = std::scoped_lock{event_recv_list_mtx}; + if (event_recv_list.size() >= event_sent_list.size()) break; + if (i > 1000) REQUIRE(! "Timeout: Waited more than one second for results"); + } + + event_sent_list.push_back( + {std::string("s/self/die@").append(tmpdir.string()), + event::effect_type::destroy, + event::path_type::watcher}); + + REQUIRE(watcher.close() == true); + REQUIRE(! fs::exists(tmpdir) || fs::remove_all(tmpdir)); + + // Check that no event for the ignored file is present + for (auto const& ev : event_recv_list) { + REQUIRE(ev.path_name != ignored_file); + REQUIRE(ev.path_name != ignored_dir); + } + + // Only events for watched_file and watcher status should be present + check_event_lists_set_eq(event_sent_list, event_recv_list); +} + diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index c7c756f0..09865663 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -944,6 +945,19 @@ inline auto is_dir(char const* const path) -> bool return stat(path, &s) == 0 && S_ISDIR(s.st_mode); } +// Helper function to check if the current dir should be skipped +inline auto should_skip = + []( + char const* const dir, + std::vector const& ignored_paths) -> bool +{ + return std::any_of( + ignored_paths.begin(), + ignored_paths.end(), + [&](std::string const& ignored) + { return strcmp(dir, ignored.c_str()) == 0; }); +}; + /* $ echo time wtr.watcher / -ms 1 | sudo bash -E ... @@ -965,7 +979,10 @@ inline auto is_dir(char const* const path) -> bool not having a full picture. */ template -inline auto walkdir_do(char const* const path, Fn const& f) -> void +inline auto walkdir_do( + char const* const path, + std::vector const& ignored_paths, + Fn const& f) -> void { if (DIR* d = opendir(path)) { f(path); @@ -976,8 +993,9 @@ inline auto walkdir_do(char const* const path, Fn const& f) -> void if (strcmp(de->d_name, ".") == 0) continue; if (strcmp(de->d_name, "..") == 0) continue; if (snprintf(next, PATH_MAX, "%s/%s", path, de->d_name) <= 0) continue; + if (should_skip(next, ignored_paths)) continue; if (! realpath(next, real)) continue; - walkdir_do(real, f); + walkdir_do(real, ignored_paths, f); } (void)closedir(d); } @@ -1074,14 +1092,19 @@ inline auto do_mark = sends diagnostics on warnings and errors. Walks the given base path, recursively, marking each directory along the way. */ -inline auto make_sysres = []( - char const* const base_path, - auto const& cb, - semabin const& living) -> sysres +inline auto make_sysres = + []( + char const* const base_path, + auto const& cb, + semabin const& living, + std::vector const& ignored_paths = {}) -> sysres { int fa_fd = fanotify_init(ke_fa_ev::init_flags, ke_fa_ev::init_io_flags); if (fa_fd < 1) return sysres{.ok = result::e_sys_api_fanotify, .il = living}; - walkdir_do(base_path, [&](auto dir) { do_mark(dir, fa_fd, cb); }); + walkdir_do( + base_path, + ignored_paths, + [&](auto dir) { do_mark(dir, fa_fd, cb); }); auto ep = make_ep(fa_fd, living.fd); if (ep.fd < 1) return close(fa_fd), sysres{.ok = result::e_sys_api_epoll, .il = living}; @@ -1232,7 +1255,11 @@ inline auto is_newdir = [](::wtr::watcher::event const& ev) -> bool The `metadata->vers` field may differ between kernel versions, so we check it against the version we were compiled with. */ -inline auto do_ev_recv = [](auto const& cb, sysres& sr) -> result +inline auto do_ev_recv = + []( + auto const& cb, + sysres& sr, + std::vector const& ignored_paths = {}) -> result { auto ev_info = [](fanotify_event_metadata const* const m) { return (fanotify_event_info_fid*)(m + 1); }; @@ -1263,7 +1290,7 @@ inline auto do_ev_recv = [](auto const& cb, sysres& sr) -> result auto r = parse_ev(mtd, read_len, &ec); if (ec) return result::w_sys_bad_fd; if (is_newdir(r.ev)) - walkdir_do(r.ev.path_name.c_str(), [&](auto dir) { + walkdir_do(r.ev.path_name.c_str(), ignored_paths, [&](auto dir) { do_mark(dir, sr.ke.fd, cb); cb({dir, r.ev.effect_type, r.ev.path_type}); }); @@ -1404,10 +1431,12 @@ inline auto do_mark = return send_msg(e, dirpath, cb), e; }; -inline auto make_sysres = []( - char const* const base_path, - auto const& cb, - semabin const& living) -> sysres +inline auto make_sysres = + []( + char const* const base_path, + auto const& cb, + semabin const& living, + std::vector const& ignored_paths = {}) -> sysres { auto make_inotify = [](result* ok) -> int { @@ -1421,7 +1450,10 @@ inline auto make_sysres = []( { auto dm = ke_in_ev::paths{}; if (*ok >= result::e) return dm; - walkdir_do(base_path, [&](auto dir) { do_mark(dir, in_fd, dm, cb); }); + walkdir_do( + base_path, + ignored_paths, + [&](auto dir) { do_mark(dir, in_fd, dm, cb); }); if (dm.empty()) *ok = result::e_self_noent; return dm; }; @@ -1596,7 +1628,11 @@ struct defer_dm_rm_wd { If this happens for some other reason, we're in trouble. */ -inline auto do_ev_recv = [](auto const& cb, sysres& sr) -> result +inline auto do_ev_recv = + []( + auto const& cb, + sysres& sr, + std::vector const& ignored_paths = {}) -> result { auto is_parity_lost = [](unsigned msk) -> bool { return msk & IN_DELETE_SELF && ! (msk & IN_MOVE_SELF); }; @@ -1627,7 +1663,7 @@ inline auto do_ev_recv = [](auto const& cb, sysres& sr) -> result else if (is_real_event(msk)) { auto parsed = parse_ev(sr.ke.dm, in_ev, in_ev_tail); if (msk & IN_ISDIR && msk & IN_CREATE) - walkdir_do(parsed.ev.path_name.c_str(), [&](auto dir) { + walkdir_do(parsed.ev.path_name.c_str(), ignored_paths, [&](auto dir) { do_mark(dir, sr.ke.fd, sr.ke.dm, cb); cb({dir, parsed.ev.effect_type, parsed.ev.path_type}); }); @@ -1659,12 +1695,16 @@ inline auto do_ev_recv = [](auto const& cb, sysres& sr) -> result namespace detail::wtr::watcher::adapter { -inline auto watch = - [](auto const& path, auto const& cb, auto const& living) -> bool +inline auto watch = []( + auto const& path, + auto const& cb, + auto const& living, + std::vector const& ignored_paths = + std::vector{}) -> bool { auto platform_watch = [&](auto make_sysres, auto do_ev_recv) -> result { - auto sr = make_sysres(path.c_str(), cb, living); + auto sr = make_sysres(path.c_str(), cb, living, ignored_paths); auto is_ev_of = [&](int nth, int fd) -> bool { return sr.ep.interests[nth].data.fd == fd; }; @@ -1678,7 +1718,7 @@ inline auto watch = if (is_ev_of(n, sr.il.fd)) sr.ok = result::complete; else if (is_ev_of(n, sr.ke.fd)) - sr.ok = do_ev_recv(cb, sr); + sr.ok = do_ev_recv(cb, sr, ignored_paths); else sr.ok = result::e_sys_api_epoll; } @@ -2253,10 +2293,11 @@ class watch { public: inline watch( std::filesystem::path const& path, - event::callback const& callback) noexcept + event::callback const& callback, + std::vector const& ignored_paths = {}) noexcept : watching{std::async( std::launch::async, - [this, path, callback] + [this, path, callback, ignored_paths] { using ::detail::wtr::watcher::adapter::watch; auto ec = std::error_code{}; @@ -2269,7 +2310,8 @@ class watch { {live_msg, event::effect_type::create, event::path_type::watcher}); - auto post_ok = pre_ok && watch(abs_path, callback, this->living); + auto post_ok = + pre_ok && watch(abs_path, callback, this->living, ignored_paths); auto die_msg = (post_ok ? "s/self/die@" : "e/self/die@") + abs_path.string(); callback( diff --git a/watcher-nodejs/lib/watcher-napi.cpp b/watcher-nodejs/lib/watcher-napi.cpp index 62a6af21..32c2631a 100644 --- a/watcher-nodejs/lib/watcher-napi.cpp +++ b/watcher-nodejs/lib/watcher-napi.cpp @@ -120,16 +120,15 @@ static napi_value close(napi_env env, napi_callback_info func_arg_info) /* Opens a watcher on a path (and any children). Calls the provided callback when events happen. - Accepts two arguments, a path and a callback. + Accepts three arguments: path, callback, and optional ignoredPaths array. Returns an object with a single method: close. - Call `.close()` when you don't want to watch - things anymore. */ + Call `.close()` when you don't want to watch things anymore. */ static napi_value watch(napi_env env, napi_callback_info func_arg_info) { - size_t argc = 2; - napi_value args[2]; + size_t argc = 3; + napi_value args[3]; napi_get_cb_info(env, func_arg_info, &argc, args, NULL, NULL); - if (argc != 2) { + if (argc < 2) { napi_throw_error(env, NULL, "Wrong number of arguments"); return NULL; } @@ -155,11 +154,49 @@ static napi_value watch(napi_env env, napi_callback_info func_arg_info) NULL, callback_js_receiver, &wrapper->tsfn); - wrapper->watcher = wtr_watcher_open(path, callback_bridge, wrapper); + + // Handle ignoredPaths argument (optional third argument) + char** ignored_paths = NULL; + size_t ignored_paths_len = 0; + if (argc >= 3) { + bool is_array = false; + napi_is_array(env, args[2], &is_array); + if (is_array) { + uint32_t arr_len = 0; + napi_get_array_length(env, args[2], &arr_len); + ignored_paths_len = arr_len; + if (arr_len > 0) { + ignored_paths = (char**)malloc(arr_len * sizeof(char*)); + for (uint32_t i = 0; i < arr_len; ++i) { + napi_value str_val; + napi_get_element(env, args[2], i, &str_val); + size_t str_len = 0; + napi_get_value_string_utf8(env, str_val, NULL, 0, &str_len); + ignored_paths[i] = (char*)malloc(str_len + 1); + napi_get_value_string_utf8( + env, str_val, ignored_paths[i], str_len + 1, &str_len + ); + } + } + } + } + + wrapper->watcher = wtr_watcher_open( + path, callback_bridge, wrapper, ignored_paths, ignored_paths_len); if (wrapper->watcher == NULL) { napi_throw_error(env, NULL, "Failed to open watcher"); + // Free ignored_paths + if (ignored_paths) { + for (size_t i = 0; i < ignored_paths_len; ++i) free(ignored_paths[i]); + free(ignored_paths); + } return NULL; } + // Free ignored_paths after watcher is created + if (ignored_paths) { + for (size_t i = 0; i < ignored_paths_len; ++i) free(ignored_paths[i]); + free(ignored_paths); + } napi_value watcher_obj = NULL; napi_create_object(env, &watcher_obj); napi_value close_func = NULL; diff --git a/watcher-nodejs/lib/watcher.ts b/watcher-nodejs/lib/watcher.ts index 4ccc6b1f..5a2fa567 100644 --- a/watcher-nodejs/lib/watcher.ts +++ b/watcher-nodejs/lib/watcher.ts @@ -35,7 +35,11 @@ interface CEvent { pathType: number; } -export const watch = (path: string, cb: (event: Event) => void): { close: () => boolean } => { +export const watch = ( + path: string, + cb: (event: Event) => void, + ignoredPaths: string[] = [] +): { close: () => boolean } => { let typedCb: null | ((_: CEvent) => void) = (cEvent) => { let event: Event = { effectTime: cEvent.effectTime, @@ -46,7 +50,7 @@ export const watch = (path: string, cb: (event: Event) => void): { close: () => }; cb(event); }; - let watcher = wcw.watch(path, typedCb); + let watcher = wcw.watch(path, typedCb, ignoredPaths); return { close: (): boolean => { if (!watcher || !typedCb) { From 3f3da815baec67d05a7243c26c3af58d6384594b Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 20 May 2025 10:59:33 +0200 Subject: [PATCH 03/13] use inotify_init1 --- include/wtr/watcher.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index a89fb581..27e285f1 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -1441,7 +1441,7 @@ inline auto make_sysres = auto make_inotify = [](result* ok) -> int { if (*ok >= result::e) return -1; - int in_fd = inotify_init(IN_CLOEXEC | IN_NONBLOCK); + int in_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); if (in_fd < 0) *ok = result::e_sys_api_inotify; return in_fd; }; From efe4a0ceeb06c3c4a6baaedf867b988e1bdcb92c Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 20 May 2025 11:20:24 +0200 Subject: [PATCH 04/13] update c api --- devel/include/wtr/watcher-/watch.hpp | 8 +++++--- watcher-c/include/wtr/watcher-c.h | 19 ++++++++++++++++++- watcher-c/src/watcher-c.cpp | 14 ++++++++++++-- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/devel/include/wtr/watcher-/watch.hpp b/devel/include/wtr/watcher-/watch.hpp index d92817d6..77e7b4c6 100644 --- a/devel/include/wtr/watcher-/watch.hpp +++ b/devel/include/wtr/watcher-/watch.hpp @@ -59,10 +59,11 @@ class watch { public: inline watch( std::filesystem::path const& path, - event::callback const& callback) noexcept + event::callback const& callback, + std::vector const& ignored_paths = {}) noexcept : watching{std::async( std::launch::async, - [this, path, callback] + [this, path, callback, ignored_paths] { using ::detail::wtr::watcher::adapter::watch; auto ec = std::error_code{}; @@ -75,7 +76,8 @@ class watch { {live_msg, event::effect_type::create, event::path_type::watcher}); - auto post_ok = pre_ok && watch(abs_path, callback, this->living); + auto post_ok = + pre_ok && watch(abs_path, callback, this->living, ignored_paths); auto die_msg = (post_ok ? "s/self/die@" : "e/self/die@") + abs_path.string(); callback( diff --git a/watcher-c/include/wtr/watcher-c.h b/watcher-c/include/wtr/watcher-c.h index 8747b0fc..7ccc3ab8 100644 --- a/watcher-c/include/wtr/watcher-c.h +++ b/watcher-c/include/wtr/watcher-c.h @@ -57,7 +57,24 @@ struct wtr_watcher_event { events and will return nothing. */ typedef void (* wtr_watcher_callback)(struct wtr_watcher_event event, void* context); -void* wtr_watcher_open(char const* const path, wtr_watcher_callback callback, void* context); +/** + * Open a watcher on a path (and any children). + * Calls the provided callback when events happen. + * Optionally accepts a list of ignored paths. + * + * @param path The root path to watch. + * @param callback The callback to invoke on events. + * @param context User data for the callback. + * @param ignored_paths Array of ignored path strings (can be NULL). + * @param ignored_paths_len Number of ignored paths (0 if none). + * @return A watcher handle, or NULL on error. + */ +void* wtr_watcher_open( + char const* const path, + wtr_watcher_callback callback, + void* context, + char const* const* ignored_paths, + size_t ignored_paths_len); bool wtr_watcher_close(void* watcher); diff --git a/watcher-c/src/watcher-c.cpp b/watcher-c/src/watcher-c.cpp index 52101fe8..732eeb77 100644 --- a/watcher-c/src/watcher-c.cpp +++ b/watcher-c/src/watcher-c.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include "wtr/watcher-c.h" #include "wtr/watcher.hpp" @@ -36,8 +38,16 @@ static int utf16_to_utf8(wchar_t const* utf16_buf, char* utf8_buf, int utf8_buf_ void* wtr_watcher_open( char const* const path, wtr_watcher_callback callback, - void* context) + void* context, + char const* const* ignored_paths, + size_t ignored_paths_len) { + std::vector ignored_vec; + if (ignored_paths && ignored_paths_len > 0) { + for (size_t i = 0; i < ignored_paths_len; ++i) { + if (ignored_paths[i]) ignored_vec.emplace_back(ignored_paths[i]); + } + } auto wrapped_callback = [callback, context](wtr::watcher::event ev_owned) { wtr_watcher_event ev_view = {}; @@ -63,7 +73,7 @@ void* wtr_watcher_open( ev_view.effect_time = ev_owned.effect_time; callback(ev_view, context); }; - return (void*)new wtr::watcher::watch(path, wrapped_callback); + return (void*)new wtr::watcher::watch(path, wrapped_callback, ignored_vec); } bool wtr_watcher_close(void* watcher) From 614ef9d215e3eaf7ef642355136c4cd60b4f29cb Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 20 May 2025 14:06:00 +0200 Subject: [PATCH 05/13] return early if path is ignored --- include/wtr/watcher.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index 27e285f1..d91e4570 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -984,6 +984,9 @@ inline auto walkdir_do( std::vector const& ignored_paths, Fn const& f) -> void { + if (should_skip(path, ignored_paths)) { + return; + } if (DIR* d = opendir(path)) { f(path); while (dirent* de = readdir(d)) { From c449c87c650627575e34ac74481694f889af2f44 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 21 May 2025 10:37:13 +0200 Subject: [PATCH 06/13] remove unrelated changes --- devel/include/wtr/watcher-/watch.hpp | 8 +- .../src/wtr/test_watcher/test_ignore_dir.cpp | 88 ------------------- include/wtr/watcher.hpp | 2 +- watcher-c/include/wtr/watcher-c.h | 12 --- 4 files changed, 4 insertions(+), 106 deletions(-) delete mode 100644 devel/src/wtr/test_watcher/test_ignore_dir.cpp diff --git a/devel/include/wtr/watcher-/watch.hpp b/devel/include/wtr/watcher-/watch.hpp index 77e7b4c6..d92817d6 100644 --- a/devel/include/wtr/watcher-/watch.hpp +++ b/devel/include/wtr/watcher-/watch.hpp @@ -59,11 +59,10 @@ class watch { public: inline watch( std::filesystem::path const& path, - event::callback const& callback, - std::vector const& ignored_paths = {}) noexcept + event::callback const& callback) noexcept : watching{std::async( std::launch::async, - [this, path, callback, ignored_paths] + [this, path, callback] { using ::detail::wtr::watcher::adapter::watch; auto ec = std::error_code{}; @@ -76,8 +75,7 @@ class watch { {live_msg, event::effect_type::create, event::path_type::watcher}); - auto post_ok = - pre_ok && watch(abs_path, callback, this->living, ignored_paths); + auto post_ok = pre_ok && watch(abs_path, callback, this->living); auto die_msg = (post_ok ? "s/self/die@" : "e/self/die@") + abs_path.string(); callback( diff --git a/devel/src/wtr/test_watcher/test_ignore_dir.cpp b/devel/src/wtr/test_watcher/test_ignore_dir.cpp deleted file mode 100644 index d8bd5545..00000000 --- a/devel/src/wtr/test_watcher/test_ignore_dir.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "snitch/snitch.hpp" -#include "test_watcher/test_watcher.hpp" -#include "wtr/watcher.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -/* Test that should_skip ignores specified directories */ - -TEST_CASE("Ignored Paths", "[dir][ignore][filter]") -{ - namespace fs = std::filesystem; - using namespace std::chrono_literals; - using namespace wtr::watcher; - using namespace wtr::test_watcher; - - auto const tmpdir = make_local_tmp_dir(); - auto const ignored_dir = tmpdir / "ignored"; - auto const watched_dir = tmpdir / "watched"; - auto event_recv_list = std::vector{}; - auto event_recv_list_mtx = std::mutex{}; - auto event_sent_list = std::vector{}; - - REQUIRE(fs::exists(tmpdir) || fs::create_directory(tmpdir)); - REQUIRE(fs::create_directory(ignored_dir)); - REQUIRE(fs::create_directory(watched_dir)); - - std::this_thread::sleep_for(10ms); - - event_sent_list.push_back( - {std::string("s/self/live@").append(tmpdir.string()), - event::effect_type::create, - event::path_type::watcher}); - - auto watcher = watch( - tmpdir, - [&event_recv_list_mtx, &event_recv_list](event const& ev) { - auto _ = std::scoped_lock{event_recv_list_mtx}; - event_recv_list.push_back(ev); - }, - {ignored_dir.string()}); - - std::this_thread::sleep_for(100ms); - - // Create a file in the ignored directory - auto ignored_file = ignored_dir / "file.txt"; - std::ofstream(ignored_file).close(); - REQUIRE(fs::is_regular_file(ignored_file)); - - // Create a file in the watched directory - auto watched_file = watched_dir / "file.txt"; - std::ofstream(watched_file).close(); - REQUIRE(fs::is_regular_file(watched_file)); - - event_sent_list.push_back( - {watched_file, event::effect_type::create, event::path_type::file}); - - // Wait for events - for (int i = 0;; i++) { - std::this_thread::sleep_for(10ms); - auto _ = std::scoped_lock{event_recv_list_mtx}; - if (event_recv_list.size() >= event_sent_list.size()) break; - if (i > 1000) REQUIRE(! "Timeout: Waited more than one second for results"); - } - - event_sent_list.push_back( - {std::string("s/self/die@").append(tmpdir.string()), - event::effect_type::destroy, - event::path_type::watcher}); - - REQUIRE(watcher.close() == true); - REQUIRE(! fs::exists(tmpdir) || fs::remove_all(tmpdir)); - - // Check that no event for the ignored file is present - for (auto const& ev : event_recv_list) { - REQUIRE(ev.path_name != ignored_file); - REQUIRE(ev.path_name != ignored_dir); - } - - // Only events for watched_file and watcher status should be present - check_event_lists_set_eq(event_sent_list, event_recv_list); -} - diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index d91e4570..1b65f1d1 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -1444,7 +1444,7 @@ inline auto make_sysres = auto make_inotify = [](result* ok) -> int { if (*ok >= result::e) return -1; - int in_fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); + int in_fd = inotify_init(); if (in_fd < 0) *ok = result::e_sys_api_inotify; return in_fd; }; diff --git a/watcher-c/include/wtr/watcher-c.h b/watcher-c/include/wtr/watcher-c.h index 7ccc3ab8..d3706f0c 100644 --- a/watcher-c/include/wtr/watcher-c.h +++ b/watcher-c/include/wtr/watcher-c.h @@ -57,18 +57,6 @@ struct wtr_watcher_event { events and will return nothing. */ typedef void (* wtr_watcher_callback)(struct wtr_watcher_event event, void* context); -/** - * Open a watcher on a path (and any children). - * Calls the provided callback when events happen. - * Optionally accepts a list of ignored paths. - * - * @param path The root path to watch. - * @param callback The callback to invoke on events. - * @param context User data for the callback. - * @param ignored_paths Array of ignored path strings (can be NULL). - * @param ignored_paths_len Number of ignored paths (0 if none). - * @return A watcher handle, or NULL on error. - */ void* wtr_watcher_open( char const* const path, wtr_watcher_callback callback, From 2d68812c18f316e41dc0e5c366247dbba9b52654 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 21 May 2025 10:53:58 +0200 Subject: [PATCH 07/13] add missing include --- watcher-c/include/wtr/watcher-c.h | 1 + 1 file changed, 1 insertion(+) diff --git a/watcher-c/include/wtr/watcher-c.h b/watcher-c/include/wtr/watcher-c.h index d3706f0c..1186de99 100644 --- a/watcher-c/include/wtr/watcher-c.h +++ b/watcher-c/include/wtr/watcher-c.h @@ -2,6 +2,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { From 76959b1d81ae08893f73923c0696d5f361ba1b3f Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 21 May 2025 14:19:33 +0200 Subject: [PATCH 08/13] fix create dir --- include/wtr/watcher.hpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index 1b65f1d1..88927c4b 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -996,7 +996,6 @@ inline auto walkdir_do( if (strcmp(de->d_name, ".") == 0) continue; if (strcmp(de->d_name, "..") == 0) continue; if (snprintf(next, PATH_MAX, "%s/%s", path, de->d_name) <= 0) continue; - if (should_skip(next, ignored_paths)) continue; if (! realpath(next, real)) continue; walkdir_do(real, ignored_paths, f); } @@ -1665,11 +1664,17 @@ inline auto do_ev_recv = send_msg(result::w_sys_q_overflow, "", cb); else if (is_real_event(msk)) { auto parsed = parse_ev(sr.ke.dm, in_ev, in_ev_tail); - if (msk & IN_ISDIR && msk & IN_CREATE) - walkdir_do(parsed.ev.path_name.c_str(), ignored_paths, [&](auto dir) { - do_mark(dir, sr.ke.fd, sr.ke.dm, cb); - cb({dir, parsed.ev.effect_type, parsed.ev.path_type}); - }); + if ( + msk & IN_ISDIR && msk & IN_CREATE + && !should_skip(parsed.ev.path_name.c_str(), ignored_paths)) + walkdir_do( + parsed.ev.path_name.c_str(), + ignored_paths, + [&](auto dir) + { + do_mark(dir, sr.ke.fd, sr.ke.dm, cb); + cb({dir, parsed.ev.effect_type, parsed.ev.path_type}); + }); else cb(parsed.ev); in_ev_next = parsed.next; From 74815c727079d3eb0e192756336f4e2c8ba0f525 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 21 May 2025 14:20:00 +0200 Subject: [PATCH 09/13] rm comment --- include/wtr/watcher.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index 88927c4b..405b8729 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -945,7 +945,6 @@ inline auto is_dir(char const* const path) -> bool return stat(path, &s) == 0 && S_ISDIR(s.st_mode); } -// Helper function to check if the current dir should be skipped inline auto should_skip = []( char const* const dir, From 44248a5dfcb9b175f2701c5d030620d6ba7c25e8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 28 May 2025 16:26:47 +0200 Subject: [PATCH 10/13] overload watch function --- include/wtr/watcher.hpp | 34 ++++++++++++++++++++++++---------- watcher-c/src/watcher-c.cpp | 2 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index 120fffa8..d55b92a0 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -2300,6 +2300,13 @@ inline namespace watcher { That's it. Happy hacking. */ + +struct opts { + std::filesystem::path path; + event::callback callback; + std::vector ignored_paths = {}; +}; + class watch { private: using sb = ::detail::wtr::watcher::semabin; @@ -2307,30 +2314,31 @@ class watch { std::future watching{}; public: - inline watch( - std::filesystem::path const& path, - event::callback const& callback, - std::vector const& ignored_paths = {}) noexcept + inline watch(opts const& options) noexcept : watching{std::async( std::launch::async, - [this, path, callback, ignored_paths] + [this, options] { using ::detail::wtr::watcher::adapter::watch; auto ec = std::error_code{}; - auto abs_path = std::filesystem::absolute(path, ec); + auto abs_path = std::filesystem::absolute(options.path, ec); auto pre_ok = ! ec && std::filesystem::is_directory(abs_path, ec) && ! ec && this->living.state() == sb::state::pending; auto live_msg = (pre_ok ? "s/self/live@" : "e/self/live@") + abs_path.string(); - callback( + options.callback( {live_msg, event::effect_type::create, event::path_type::watcher}); - auto post_ok = - pre_ok && watch(abs_path, callback, this->living, ignored_paths); + auto post_ok = pre_ok + && watch( + abs_path, + options.callback, + this->living, + options.ignored_paths); auto die_msg = (post_ok ? "s/self/die@" : "e/self/die@") + abs_path.string(); - callback( + options.callback( {die_msg, event::effect_type::destroy, event::path_type::watcher}); @@ -2338,6 +2346,12 @@ class watch { })} {} + inline watch( + std::filesystem::path const& path, + event::callback const& callback) noexcept + : watch(opts{path, callback}) + {} + inline auto close() noexcept -> bool { return this->living.release() != sb::state::error diff --git a/watcher-c/src/watcher-c.cpp b/watcher-c/src/watcher-c.cpp index 732eeb77..2265c6a0 100644 --- a/watcher-c/src/watcher-c.cpp +++ b/watcher-c/src/watcher-c.cpp @@ -73,7 +73,7 @@ void* wtr_watcher_open( ev_view.effect_time = ev_owned.effect_time; callback(ev_view, context); }; - return (void*)new wtr::watcher::watch(path, wrapped_callback, ignored_vec); + return (void*)new wtr::watcher::watch({path, wrapped_callback, ignored_vec}); } bool wtr_watcher_close(void* watcher) From 116ba9e0cc47bff4c8c35db021fa4e00fa664984 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 28 May 2025 16:49:34 +0200 Subject: [PATCH 11/13] use set instead of vec --- include/wtr/watcher.hpp | 30 +++++++++++------------------- watcher-c/src/watcher-c.cpp | 8 ++++---- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index d55b92a0..9c765abc 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -4,12 +4,12 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -946,16 +946,8 @@ inline auto is_dir(char const* const path) -> bool } inline auto should_skip = - []( - char const* const dir, - std::vector const& ignored_paths) -> bool -{ - return std::any_of( - ignored_paths.begin(), - ignored_paths.end(), - [&](std::string const& ignored) - { return strcmp(dir, ignored.c_str()) == 0; }); -}; + [](char const* const dir, std::set const& ignored_paths) -> bool +{ return ignored_paths.find(dir) != ignored_paths.end(); }; /* $ echo time wtr.watcher / -ms 1 | sudo bash -E @@ -980,7 +972,7 @@ inline auto should_skip = template inline auto walkdir_do( char const* const path, - std::vector const& ignored_paths, + std::set const& ignored_paths, Fn const& f) -> void { if (should_skip(path, ignored_paths)) { @@ -1098,7 +1090,7 @@ inline auto make_sysres = char const* const base_path, auto const& cb, semabin const& living, - std::vector const& ignored_paths = {}) -> sysres + std::set const& ignored_paths = {}) -> sysres { int fa_fd = fanotify_init(ke_fa_ev::init_flags, ke_fa_ev::init_io_flags); if (fa_fd < 1) return sysres{.ok = result::e_sys_api_fanotify, .il = living}; @@ -1264,7 +1256,7 @@ inline auto do_ev_recv = []( auto const& cb, sysres& sr, - std::vector const& ignored_paths = {}) -> result + std::set const& ignored_paths = {}) -> result { auto ev_info = [](fanotify_event_metadata const* const m) { return (fanotify_event_info_fid*)(m + 1); }; @@ -1441,7 +1433,7 @@ inline auto make_sysres = char const* const base_path, auto const& cb, semabin const& living, - std::vector const& ignored_paths = {}) -> sysres + std::set const& ignored_paths = {}) -> sysres { auto make_inotify = [](result* ok) -> int { @@ -1641,7 +1633,7 @@ inline auto do_ev_recv = []( auto const& cb, sysres& sr, - std::vector const& ignored_paths = {}) -> result + std::set const& ignored_paths = {}) -> result { auto is_parity_lost = [](unsigned msk) -> bool { return msk & IN_DELETE_SELF && ! (msk & IN_MOVE_SELF); }; @@ -1714,8 +1706,8 @@ inline auto watch = []( auto const& path, auto const& cb, auto const& living, - std::vector const& ignored_paths = - std::vector{}) -> bool + std::set const& ignored_paths = + std::set{}) -> bool { auto platform_watch = [&](auto make_sysres, auto do_ev_recv) -> result { @@ -2304,7 +2296,7 @@ inline namespace watcher { struct opts { std::filesystem::path path; event::callback callback; - std::vector ignored_paths = {}; + std::set ignored_paths = {}; }; class watch { diff --git a/watcher-c/src/watcher-c.cpp b/watcher-c/src/watcher-c.cpp index 2265c6a0..ba37bf38 100644 --- a/watcher-c/src/watcher-c.cpp +++ b/watcher-c/src/watcher-c.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include "wtr/watcher-c.h" #include "wtr/watcher.hpp" @@ -42,10 +42,10 @@ void* wtr_watcher_open( char const* const* ignored_paths, size_t ignored_paths_len) { - std::vector ignored_vec; + std::set ignored_set; if (ignored_paths && ignored_paths_len > 0) { for (size_t i = 0; i < ignored_paths_len; ++i) { - if (ignored_paths[i]) ignored_vec.emplace_back(ignored_paths[i]); + if (ignored_paths[i]) ignored_set.emplace(ignored_paths[i]); } } auto wrapped_callback = [callback, context](wtr::watcher::event ev_owned) @@ -73,7 +73,7 @@ void* wtr_watcher_open( ev_view.effect_time = ev_owned.effect_time; callback(ev_view, context); }; - return (void*)new wtr::watcher::watch({path, wrapped_callback, ignored_vec}); + return (void*)new wtr::watcher::watch({path, wrapped_callback, ignored_set}); } bool wtr_watcher_close(void* watcher) From 05584531021d4fec56a2fd90a4d4524501cbc899 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 28 May 2025 16:59:06 +0200 Subject: [PATCH 12/13] move struct --- include/wtr/watcher.hpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index 9c765abc..b8d01431 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -2249,6 +2249,12 @@ inline auto watch( namespace wtr { inline namespace watcher { +struct opts { + std::filesystem::path path; + event::callback callback; + std::set ignored_paths = {}; +}; + /* An asynchronous filesystem watcher. Begins watching when constructed. @@ -2292,13 +2298,6 @@ inline namespace watcher { That's it. Happy hacking. */ - -struct opts { - std::filesystem::path path; - event::callback callback; - std::set ignored_paths = {}; -}; - class watch { private: using sb = ::detail::wtr::watcher::semabin; From bf3252555d0a3d46b14d931ac560a33da041a877 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 3 Jun 2025 12:12:51 +0200 Subject: [PATCH 13/13] add ignored paths for other backends --- include/wtr/watcher.hpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/include/wtr/watcher.hpp b/include/wtr/watcher.hpp index b8d01431..dd80dda1 100644 --- a/include/wtr/watcher.hpp +++ b/include/wtr/watcher.hpp @@ -799,7 +799,8 @@ close_event_stream(FSEventStreamRef stream, ContextData& ctx) -> bool inline auto watch( std::filesystem::path const& path, ::wtr::watcher::event::callback const& cb, - semabin const& living) -> bool + semabin const& living, + std::set const& ignored_paths = std::set{}) -> bool { auto seen_created_paths = ContextData::pathset{}; auto last_rename_path = ContextData::fspath{}; @@ -1997,7 +1998,9 @@ inline auto do_event_send( inline auto watch( std::filesystem::path const& path, ::wtr::watcher::event::callback const& callback, - semabin const& living) noexcept -> bool + semabin const& living, + std::set const& ignored_paths = std::set{}) noexcept + -> bool { using namespace ::wtr::watcher; auto w = watch_event_proxy{path}; @@ -2217,7 +2220,9 @@ inline bool tend_bucket( inline auto watch( std::filesystem::path const& path, ::wtr::watcher::event::callback const& callback, - semabin const& living) noexcept -> bool + semabin const& living, + std::set const& ignored_paths = std::set{}) noexcept + -> bool { using std::this_thread::sleep_for; using namespace std::chrono_literals;