/** * bookmarkfs/tests/check_watcher.c * ---- * * Copyright (C) 2025 CismonX * * 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 Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include #include #include #include #include #include #include "check_lib.h" #include "frontend_util.h" #include "sandbox.h" #include "watcher.h" // Forward declaration start static int do_check_watcher (int, uint32_t); static void msecs_to_timespec (struct timespec *, unsigned long); static int wait_for_watcher (struct watcher *, struct timespec const *, int); // Forward declaration end static int do_check_watcher ( int dirfd, uint32_t flags ) { #define FILE1_NAME "foo.tmp" #define FILE2_NAME "bar.tmp" #define ASSERT_EQ(val, expr) ASSERT_EXPR_INT(expr, r_, (val) == r_, goto end;) #define ASSERT_NE(val, expr) ASSERT_EXPR_INT(expr, r_, (val) != r_, goto end;) unsigned long msecs = 50; int tries = 10; if (flags & WATCHER_FALLBACK) { msecs = 2500; tries = 2; } struct timespec ts; msecs_to_timespec(&ts, msecs); int status = -1; int fd = -1; struct watcher *w = NULL; fd = openat(dirfd, FILE1_NAME, O_WRONLY | O_CREAT, 0600); ASSERT_NE(-1, fd); w = watcher_create(dirfd, FILE1_NAME, flags); if (w == NULL) { goto end; } // Check for spurious zero returns. ASSERT_EQ(-ETIMEDOUT, wait_for_watcher(w, &ts, tries)); ASSERT_NE(-1, write(fd, "foo", 3)); ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); bool check_truncate = true; #if defined(__FreeBSD__) // For kevent() EVFILT_VNODE, ftruncate() only triggers NOTE_ATTRIB, // which we don't want to watch. if (!(flags & WATCHER_FALLBACK)) { check_truncate = false; } #endif if (check_truncate) { ASSERT_EQ(0, ftruncate(fd, 0)); ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); } int fd2 = openat(dirfd, FILE2_NAME, O_WRONLY | O_CREAT, 0600); ASSERT_NE(-1, fd2); close(fd2); bool close_fd = false; #if defined(__linux__) // FAN_DELETE_SELF won't fire if the watched file // is still opened somewhere. if (!(flags & WATCHER_FALLBACK)) { close_fd = true; } #endif if (close_fd) { close(fd); fd = -1; } ASSERT_EQ(0, renameat(dirfd, FILE2_NAME, dirfd, FILE1_NAME)); ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); ASSERT_EQ(0, renameat(dirfd, FILE1_NAME, dirfd, FILE2_NAME)); ASSERT_EQ(-ENOENT, wait_for_watcher(w, &ts, tries)); // If the watched file has gone, but managed to come back, // the watcher should continue to work. ASSERT_EQ(0, renameat(dirfd, FILE2_NAME, dirfd, FILE1_NAME)); ASSERT_EQ(0, wait_for_watcher(w, &ts, tries)); ASSERT_EQ(0, unlinkat(dirfd, FILE1_NAME, 0)); ASSERT_EQ(-ENOENT, wait_for_watcher(w, &ts, tries)); status = 0; end: if (fd >= 0) { close(fd); } unlinkat(dirfd, FILE1_NAME, 0); unlinkat(dirfd, FILE2_NAME, 0); watcher_destroy(w); return status; } static void msecs_to_timespec ( struct timespec *ts_buf, unsigned long millisecs ) { ts_buf->tv_sec = millisecs / 1000; ts_buf->tv_nsec = (millisecs % 1000) * 1000000; } static int wait_for_watcher ( struct watcher *w, struct timespec const *ts, int retry ) { for (int i = 0; i < retry; ++i) { clock_nanosleep(CLOCK_MONOTONIC, 0, ts, NULL); int status = watcher_poll(w); if (status != -EAGAIN) { return status; } } return -ETIMEDOUT; } int check_watcher ( int argc, char *argv[] ) { uint32_t flags = 0; #if defined(__FreeBSD__) // Do not enable sandbox on FreeBSD, // since the watcher sandbox only grants read access to dirfd, // and cap_enter() applies to the entire process. flags |= SANDBOX_NOOP << WATCHER_SANDBOX_FLAGS_OFFSET; #endif getopt_foreach(argc, argv, ":f") { case 'f': flags |= WATCHER_FALLBACK; break; default: log_printf("bad option '-%c'", optopt); return -1; } argc -= optind; if (argc < 1) { log_puts("path not specified"); return -1; } argv += optind; int dirfd = open(argv[0], O_RDONLY | O_DIRECTORY); if (dirfd < 0) { log_printf("failed to open '%s'", argv[0]); return -1; } int status = do_check_watcher(dirfd, flags); close(dirfd); return status; }