Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/reactor_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:
ln -s "$PWD/cf-reactor/cf-reactor" "$PWD/cf-promises/cf-promises" "$RUNNER_TEMP/bin/"
cd tests/reactor
chmod -R go-w .
CF_BINDIR="$RUNNER_TEMP/bin" sh run.sh
CF_BINDIR="$RUNNER_TEMP/bin" sh run.sh --unsafe
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ stamp-h1
/tests/acceptance/25_cf-execd/Makefile
/tests/acceptance/Makefile
/tests/load/Makefile
/tests/reactor/Makefile
/tests/unit/Makefile
/tests/static-check/Makefile
/tests/valgrind-check/Makefile
Expand Down
2 changes: 2 additions & 0 deletions cf-reactor/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ libcf_reactor_la_SOURCES = \
reactor_context.c reactor_context.h \
reactor_transform.c reactor_transform.h \
stoppable_thread.c stoppable_thread.h \
user_watcher.c user_watcher.h \
wakeup_channel.c wakeup_channel.h \
watch_backends.c watch_backends.h \
watcher.c watcher.h

if !BUILTIN_EXTENSIONS
Expand Down
102 changes: 98 additions & 4 deletions cf-reactor/file_watcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <prototypes3.h> /* cf_strtimestamp_local() */
#include <sys/stat.h>
#include <errno.h>
#include <string.h> /* strerror() */
#include <openssl/evp.h>

/* Minimum time between two logs of a persisting failure to check the file,
Expand All @@ -40,6 +41,16 @@

#define CONTENT_HASH_METHOD HASH_METHOD_SHA256

/* What changes when a file's content may have changed, see StatSignatureOf() */
typedef struct
{
dev_t dev;
ino_t ino;
off_t size;
struct timespec mtime;
struct timespec ctime;
} StatSignature;

typedef struct
{
char *path;
Expand All @@ -60,6 +71,7 @@ typedef struct
{
bool hashed; /* Whether it was a regular file, that was hashed */
unsigned char digest[EVP_MAX_MD_SIZE + 1];
StatSignature signature; /* of the file when it was hashed */
} content;

/* FileWatcherCheckFilePermissionsChanged() */
Expand All @@ -81,14 +93,41 @@ typedef struct
} last;
} FileWatcherState;

/**
* Copy of path in a form stat() accepts. On Windows, msvcrt's stat() fails
* with ENOENT for a path with a trailing separator, or with the \\?\ (or
* //?/) prefix, so a watcher with such a path would never see its file.
*/
static char *WatchPathCopy(const char *path)
{
assert(path != NULL);

char *copy = xstrdup(path);
#ifdef __MINGW32__
if ((copy[0] == '\\' || copy[0] == '/') && (copy[1] == '\\' || copy[1] == '/') &&
copy[2] == '?' && (copy[3] == '\\' || copy[3] == '/'))
{
memmove(copy, copy + 4, strlen(copy + 4) + 1);
}

/* Keeps the separator of a drive's root, e.g. C:/ */
size_t length = strlen(copy);
while (length > 1 && (copy[length - 1] == '\\' || copy[length - 1] == '/') &&
!(length == 3 && copy[1] == ':'))
{
copy[--length] = '\0';
}
#endif
return copy;
}

void *FileWatcherStateNew(const Rlist *args)
{
assert(args != NULL);
const char *path = RlistScalarValue(args);

FileWatcherState *fws = (FileWatcherState *) xcalloc(1, sizeof(FileWatcherState));
fws->path = xstrdup(path);
fws->path = WatchPathCopy(path);

return fws;
}
Expand All @@ -100,7 +139,7 @@ void *FileWatcherSizeStateNew(const Rlist *args)
const long size_threshold = IntFromString(RlistScalarValue(args->next));

FileWatcherState *fws = (FileWatcherState *) xcalloc(1, sizeof(FileWatcherState));
fws->path = xstrdup(path);
fws->path = WatchPathCopy(path);
fws->last.file_size.threshold = size_threshold;

return fws;
Expand Down Expand Up @@ -129,8 +168,10 @@ static void LogCheckError(FileWatcherState *fws, const char *op, int err)
if (fws->last_error_logged == 0 ||
now - fws->last_error_logged >= CHECK_ERROR_LOG_INTERVAL_SECS)
{
/* strerror(), as err is an errno value: GetErrorStrFromCode()
* takes a Windows error code on Windows */
Log(LOG_LEVEL_ERR, "Unable to %s '%s' while checking for file events: %s",
op, fws->path, GetErrorStrFromCode(err));
op, fws->path, strerror(err));
fws->last_error_logged = now;
}
}
Expand All @@ -155,6 +196,19 @@ static bool FileLstat(FileWatcherState *fws, bool *exists, struct stat *sb)
return true;
}

#ifdef __MINGW32__
/* msvcrt's stat() also fails with ENOENT while the file's directory is
* open without sharing (FindFirstFile() gets a sharing violation), when
* the file is still there: then whether it changed is unknown */
WIN32_FILE_ATTRIBUTE_DATA attributes;
if (errno == ENOENT &&
GetFileAttributesExA(fws->path, GetFileExInfoStandard, &attributes))
{
LogCheckError(fws, "lstat", EBUSY);
return false;
}
#endif

if (errno == ENOENT || errno == ENOTDIR)
{
*exists = false;
Expand Down Expand Up @@ -280,6 +334,37 @@ WatcherCheckResult FileWatcherCheckFileModified(void *state)

// =========== code for the file_content_modified event ===========

static struct timespec StatCtime(const struct stat *sb)
{
assert(sb != NULL);

#if defined(HAVE_STRUCT_STAT_ST_MTIM)
return sb->st_ctim;
#elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC)
return sb->st_ctimespec;
#else
return (struct timespec) { .tv_sec = sb->st_ctime, .tv_nsec = 0 };
#endif
}

/* Content can't change without one of these changing too: writing updates
* mtime and ctime, replacing the file changes the inode, and setting mtime
* back still updates ctime. So the file is only hashed again when they
* differ, which keeps frequent checks of many files cheap. */
static StatSignature StatSignatureOf(const struct stat *sb)
{
assert(sb != NULL);

StatSignature signature;
memset(&signature, 0, sizeof(signature)); /* no padding garbage for memcmp() */
signature.dev = sb->st_dev;
signature.ino = sb->st_ino;
signature.size = sb->st_size;
signature.mtime = StatMtime(sb);
signature.ctime = StatCtime(sb);
return signature;
}

/* Hashes the content of the file into digest. Returns false and sets errno
* on failure. */
static bool HashFileContent(const char *path, unsigned char digest[EVP_MAX_MD_SIZE + 1])
Expand Down Expand Up @@ -335,7 +420,15 @@ WatcherCheckResult FileWatcherCheckFileContentModified(void *state)

bool hashed = false;
unsigned char digest[EVP_MAX_MD_SIZE + 1] = { 0 };
if (exists && S_ISREG(sb.st_mode))
const StatSignature signature = exists ? StatSignatureOf(&sb) : (StatSignature) { 0 };
if (exists && S_ISREG(sb.st_mode) && fws->last.content.hashed &&
memcmp(&fws->last.content.signature, &signature, sizeof(signature)) == 0)
{
/* Unchanged since hashed, see StatSignatureOf() */
hashed = true;
memcpy(digest, fws->last.content.digest, sizeof(digest));
}
else if (exists && S_ISREG(sb.st_mode))
{
if (HashFileContent(fws->path, digest))
{
Expand All @@ -354,6 +447,7 @@ WatcherCheckResult FileWatcherCheckFileContentModified(void *state)

fws->last.content.hashed = hashed;
memcpy(fws->last.content.digest, digest, sizeof(digest));
fws->last.content.signature = signature;
return FileWatcherRecordObservation(fws, exists, modified, "content modification");
}

Expand Down
57 changes: 51 additions & 6 deletions cf-reactor/reactor_transform.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include <mod_reactor.h> // REACTOR_INTERVAL_RANGE, REACTOR_FILE_DELETED, REACTOR_*_TYPES, BundleIsReactorEventsBundle()
#include <syntax.h> // CheckConstraintTypeMatch(), PromiseTypeSyntaxGet(), BodySyntaxGetConstraintSyntax()
#include <file_watcher.h>
#include <user_watcher.h>
#include <conversion.h>

/* Resolves the agent, common or reactor bundle (looked up in that order)
Expand Down Expand Up @@ -147,6 +148,7 @@ typedef struct
WatcherStateNewFn new_state;
WatcherStateDestroyFn destroy_state;
WatcherCheckFn check;
ReactorWatchTargetType target; /* what the 'name' constraint is about */
} EventType;

static const EventArg NO_EXTRA_ARGS[] =
Expand All @@ -160,13 +162,20 @@ static const EventArg FILE_SIZE_EXCEEDS_ARGS[] =
{ NULL, RVAL_TYPE_NOPROMISEE }
};

#define EventTypeNew(name, type, args, new_state, destroy_state, check) \
{ name, type, args, new_state, destroy_state, check }
#define EventTypeNewNull() EventTypeNew(NULL, RVAL_TYPE_NOPROMISEE, NULL, NULL, NULL, NULL)
#define EventTypeNew(name, type, args, new_state, destroy_state, check, target) \
{ name, type, args, new_state, destroy_state, check, target }
#define EventTypeNewNull() \
EventTypeNew(NULL, RVAL_TYPE_NOPROMISEE, NULL, NULL, NULL, NULL, REACTOR_WATCH_TARGET_FILE)

/* A file event, about the file at the path of its constraint */
#define FileEventTypeNew(name, check) \
EventTypeNew(name, RVAL_TYPE_SCALAR, NO_EXTRA_ARGS, FileWatcherStateNew, FileWatcherStateDestroy, check)
EventTypeNew(name, RVAL_TYPE_SCALAR, NO_EXTRA_ARGS, FileWatcherStateNew, FileWatcherStateDestroy, \
check, REACTOR_WATCH_TARGET_FILE)

/* A user event, about the user with the name of its constraint */
#define UserEventTypeNew(name, check) \
EventTypeNew(name, RVAL_TYPE_SCALAR, NO_EXTRA_ARGS, UserWatcherStateNew, UserWatcherStateDestroy, \
check, REACTOR_WATCH_TARGET_USERS)

static const EventType EVENT_TABLE[] =
{
Expand All @@ -177,7 +186,10 @@ static const EventType EVENT_TABLE[] =
FileEventTypeNew(REACTOR_FILE_PERMISSIONS_CHANGED, FileWatcherCheckFilePermissionsChanged),
FileEventTypeNew(REACTOR_FILE_OWNER_CHANGED, FileWatcherCheckFileOwnerChanged),
EventTypeNew(REACTOR_FILE_SIZE_EXCEEDS, RVAL_TYPE_SCALAR, FILE_SIZE_EXCEEDS_ARGS,
FileWatcherSizeStateNew, FileWatcherStateDestroy, FileWatcherCheckFileSizeExceeds),
FileWatcherSizeStateNew, FileWatcherStateDestroy, FileWatcherCheckFileSizeExceeds,
REACTOR_WATCH_TARGET_FILE),
UserEventTypeNew(REACTOR_USER_ADDED, UserWatcherCheckUserAdded),
UserEventTypeNew(REACTOR_USER_REMOVED, UserWatcherCheckUserRemoved),
EventTypeNewNull()
};

Expand Down Expand Up @@ -264,6 +276,31 @@ static bool EventsPromiseGetInterval(const Promise *pp, time_t *interval)
return true;
}

/* Sets *methods to the 'watch_methods' of the (expanded) events promise, NULL
* if it has none. The method names were checked against the syntax already,
* on expansion. */
static bool EventsPromiseGetWatchMethods(const Promise *pp, const Rlist **methods)
{
assert(pp != NULL);
assert(methods != NULL);

*methods = NULL;
const Constraint *con = PromiseGetConstraint(pp, REACTOR_WATCH_METHODS);
if (con == NULL)
{
return true;
}
if (con->rval.type != RVAL_TYPE_LIST)
{
Log(LOG_LEVEL_ERR,
"Reactor events promise '%s' has an invalid '%s', expected a list of methods",
pp->promiser, REACTOR_WATCH_METHODS);
return false;
}
*methods = RvalRlistValue(con->rval);
return true;
}

static bool ResolveWhenBody(const Promise *pp, WatcherOptions *opt)
{
assert(pp != NULL);
Expand Down Expand Up @@ -310,6 +347,7 @@ static bool ResolveWhenBody(const Promise *pp, WatcherOptions *opt)
if (BodySyntaxGetConstraintSyntax(when_syntax, cp->lval) == NULL || /* not a 'when' constraint */
StringEqual(cp->lval, event->name) ||
StringEqual(cp->lval, "interval") ||
StringEqual(cp->lval, REACTOR_WATCH_METHODS) ||
StringEqual(cp->lval, "meta")) /* also a promise attribute */
{
continue;
Expand All @@ -330,12 +368,19 @@ static bool ResolveWhenBody(const Promise *pp, WatcherOptions *opt)
}
}

if (!EventsPromiseGetInterval(pp, &opt->interval))
if (!EventsPromiseGetInterval(pp, &opt->interval) ||
!EventsPromiseGetWatchMethods(pp, &opt->methods))
{
RlistDestroy(args);
return false;
}

/* The path is the value of the event's constraint, which lives as long
* as pp, unlike args */
opt->target.type = event->target;
opt->target.path = (event->target == REACTOR_WATCH_TARGET_FILE) ?
RvalScalarValue(PromiseGetConstraint(pp, event->name)->rval) : NULL;

opt->state = event->new_state(args);
opt->check_callback = event->check;
opt->destroy_state = event->destroy_state;
Expand Down
Loading
Loading