diff --git a/cf-reactor/Makefile.am b/cf-reactor/Makefile.am index 3dc15308837..9961083ee36 100644 --- a/cf-reactor/Makefile.am +++ b/cf-reactor/Makefile.am @@ -39,10 +39,11 @@ libcf_reactor_la_LIBADD = ../libpromises/libpromises.la libcf_reactor_la_SOURCES = \ cf-reactor.c \ + file_watcher.c file_watcher.h \ reactor_context.c reactor_context.h \ - watcher.c watcher.h \ stoppable_thread.c stoppable_thread.h \ - wakeup_channel.c wakeup_channel.h + wakeup_channel.c wakeup_channel.h \ + watcher.c watcher.h if !BUILTIN_EXTENSIONS bin_PROGRAMS = cf-reactor diff --git a/cf-reactor/file_watcher.c b/cf-reactor/file_watcher.c new file mode 100644 index 00000000000..72e50856fd1 --- /dev/null +++ b/cf-reactor/file_watcher.c @@ -0,0 +1,124 @@ +/* + Copyright 2026 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + 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; version 3. + + 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, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#include +#include +#include +#include +#include /* cf_strtimestamp_local() */ +#include +#include + +// =========== code for EVENT_FILE_DELETED event type =========== + +typedef struct +{ + char *path; + time_t last_seen; /* Time the file was last observed present, 0 if absent */ +} FileWatcherState; + +/* Sets *deleted to whether the path exists. Only ENOENT/ENOTDIR mean the path + * genuinely doesn't exist. Any other lstat() failure (e.g. EACCES, ESTALE) is + * a transient/permission error, in which case we can't tell whether the file + * was deleted, so false is returned and *deleted is left untouched. + * + * Doesn't follow symlinks, i.e. a dangling symlink is not considered deleted */ +static bool FileDeleted(const char *path, bool *deleted) +{ + assert(deleted != NULL); + + struct stat sb; + if (lstat(path, &sb) == 0) + { + *deleted = false; + return true; + } + + if (errno == ENOENT || errno == ENOTDIR) + { + *deleted = true; + return true; + } + + Log(LOG_LEVEL_ERR, "Unable to lstat '%s' while checking for file deletion: %s", path, GetErrorStr()); + return false; +} + +void *FileWatcherStateNew(const char *path) +{ + FileWatcherState *fws = (FileWatcherState *) xmalloc(sizeof(FileWatcherState)); + + fws->path = xstrdup(path); + + /* If the file's presence can't be determined, treat it as not yet seen */ + bool deleted; + fws->last_seen = (FileDeleted(path, &deleted) && !deleted) ? time(NULL) : 0; + + return (void *) fws; +} + +bool CheckFileDeleted(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool deleted_now; + if (!FileDeleted(fws->path, &deleted_now)) + { + /* Unknown state, skip this check and keep last_seen as is */ + return false; + } + + const time_t now = time(NULL); + const bool deleted = (fws->last_seen != 0) && deleted_now; + + if (deleted) + { + char last_seen_str[26]; + const char *last_seen_ts = cf_strtimestamp_local(fws->last_seen, last_seen_str); + + char now_str[26]; + const char *now_ts = cf_strtimestamp_local(now, now_str); + + Log(LOG_LEVEL_VERBOSE, + "Observed deletion of file '%s' at %s (last seen present at %s)", + fws->path, + (now_ts != NULL) ? now_ts : "unknown time", + (last_seen_ts != NULL) ? last_seen_ts : "unknown time"); + } + + fws->last_seen = deleted_now ? 0 : now; + return deleted; +} + +void DestroyFileWatcherState(void *state) +{ + FileWatcherState *fws = state; + if (fws != NULL) + { + free(fws->path); + free(fws); + } +} + diff --git a/cf-reactor/file_watcher.h b/cf-reactor/file_watcher.h new file mode 100644 index 00000000000..053475ae52c --- /dev/null +++ b/cf-reactor/file_watcher.h @@ -0,0 +1,35 @@ +/* + Copyright 2026 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + 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; version 3. + + 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, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#ifndef CFENGINE_FILE_WATCHER_H +#define CFENGINE_FILE_WATCHER_H + +#include + + +void *FileWatcherStateNew(const char *path); +bool CheckFileDeleted(void *state); +void DestroyFileWatcherState(void *state); + +#endif diff --git a/cf-reactor/watcher.c b/cf-reactor/watcher.c index 28fda475cdd..c2b693dc653 100644 --- a/cf-reactor/watcher.c +++ b/cf-reactor/watcher.c @@ -33,6 +33,7 @@ #include #include #include +#include /* Upper bound on how long the watcher thread ever sleeps in one go, so that * IsPendingTermination() is re-checked at least this often during shutdown, @@ -94,11 +95,13 @@ void WatcherRegister(const char *key, EventType type, void *state, Bundle *bundl WatcherStateDestroyFn destroy_state = NULL; switch (type) { - case EVENT_FILE_DELETED: - // TODO: initialize check_callback and destroy_state + check_callback = CheckFileDeleted; + destroy_state = DestroyFileWatcherState; break; + // TODO: add more event types + default: ProgrammingError("Unknown reactor event type %d for watcher '%s'", (int) type, key); } diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am index 2798f60d74b..e2eb77b7098 100644 --- a/tests/unit/Makefile.am +++ b/tests/unit/Makefile.am @@ -232,6 +232,7 @@ regex_test_SOURCES = regex_test.c ../../libpromises/match_scope.c watcher_test_SOURCES = watcher_test.c \ ../../cf-reactor/watcher.c \ + ../../cf-reactor/file_watcher.c \ ../../cf-reactor/stoppable_thread.c \ ../../cf-reactor/wakeup_channel.c diff --git a/tests/unit/watcher_test.c b/tests/unit/watcher_test.c index 8991d7d9f53..99064e0db18 100644 --- a/tests/unit/watcher_test.c +++ b/tests/unit/watcher_test.c @@ -1,6 +1,7 @@ #include #include +#include /* FileWatcherStateNew() */ #include /* Bundle */ #include /* LoggingPrivContext, LoggingPrivSetContext() */ @@ -34,7 +35,7 @@ static void test_watcher_register_single(void) * fine here. */ Bundle *fake_bundle = (Bundle *) 0x1; - WatcherRegister("test-event", EVENT_FILE_DELETED, NULL, fake_bundle, 5); + WatcherRegister("test-event", EVENT_FILE_DELETED, FileWatcherStateNew("/nonexistent/test-event"), fake_bundle, 5); WatcherRegistryFinalize(); } @@ -46,7 +47,7 @@ static void test_watcher_register_duplicate_key_ignored(void) Bundle *fake_bundle_a = (Bundle *) 0x1; Bundle *fake_bundle_b = (Bundle *) 0x2; - WatcherRegister("dup-event", EVENT_FILE_DELETED, NULL, fake_bundle_a, 5); + WatcherRegister("dup-event", EVENT_FILE_DELETED, FileWatcherStateNew("/nonexistent/dup-event-a"), fake_bundle_a, 5); captured_err_count = 0; captured_err_message[0] = '\0'; @@ -56,7 +57,7 @@ static void test_watcher_register_duplicate_key_ignored(void) /* Registering the same key again must be rejected: an error is logged * (not silently swallowed) and the first registration is kept, not * replaced. */ - WatcherRegister("dup-event", EVENT_FILE_DELETED, NULL, fake_bundle_b, 5); + WatcherRegister("dup-event", EVENT_FILE_DELETED, FileWatcherStateNew("/nonexistent/dup-event-b"), fake_bundle_b, 5); LoggingPrivSetContext(NULL);