Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cf-reactor/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
124 changes: 124 additions & 0 deletions cf-reactor/file_watcher.c
Original file line number Diff line number Diff line change
@@ -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 <file_watcher.h>
#include <watcher.h>
#include <logging.h>
#include <alloc.h>
#include <prototypes3.h> /* cf_strtimestamp_local() */
#include <sys/stat.h>
#include <errno.h>

// =========== 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);
}
}

35 changes: 35 additions & 0 deletions cf-reactor/file_watcher.h
Original file line number Diff line number Diff line change
@@ -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 <platform.h>


void *FileWatcherStateNew(const char *path);
bool CheckFileDeleted(void *state);
void DestroyFileWatcherState(void *state);

#endif
7 changes: 5 additions & 2 deletions cf-reactor/watcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <map.h>
#include <sequence.h>
#include <threaded_queue.h>
#include <file_watcher.h>

/* 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,
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions tests/unit/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 4 additions & 3 deletions tests/unit/watcher_test.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <test.h>

#include <watcher.h>
#include <file_watcher.h> /* FileWatcherStateNew() */
#include <cf3.defs.h> /* Bundle */
#include <logging_priv.h> /* LoggingPrivContext, LoggingPrivSetContext() */

Expand Down Expand Up @@ -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();
}
Expand All @@ -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';
Expand All @@ -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);

Expand Down
Loading