From 6dd68921a624dbad8603a92a6197dedceb73e82e Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 14 Oct 2026 14:46:17 +0200 Subject: [PATCH 1/3] Added modular interface for event types Moved from a hardcoded and single `file_deleted` event type, to a table with all the event types and their necessary code, so that cf-reactor can easiky be extended with new event types. Ticket: ENT-14692 Signed-off-by: Victor Moene --- cf-reactor/file_watcher.c | 159 +++++++++++++++------------ cf-reactor/file_watcher.h | 4 +- cf-reactor/reactor_transform.c | 190 +++++++++++++++++++++++++++------ cf-reactor/watcher.c | 30 +++--- cf-reactor/watcher.h | 17 +-- libpromises/mod_reactor.c | 6 +- libpromises/mod_reactor.h | 2 + 7 files changed, 289 insertions(+), 119 deletions(-) diff --git a/cf-reactor/file_watcher.c b/cf-reactor/file_watcher.c index 8a82d06dff1..1fec26adf37 100644 --- a/cf-reactor/file_watcher.c +++ b/cf-reactor/file_watcher.c @@ -30,114 +30,141 @@ #include #include -// =========== code for EVENT_FILE_DELETED event type =========== - -/* Minimum time between two logs of a persisting lstat() failure, since the - * file is checked on every poll */ -#define LSTAT_ERROR_LOG_INTERVAL_SECS 60 +/* Minimum time between two logs of a persisting failure to check the file, + * since the file is checked on every poll */ +#define CHECK_ERROR_LOG_INTERVAL_SECS 60 typedef struct { char *path; - time_t last_seen; /* Time the file was last observed present, 0 if absent */ - time_t lstat_error_logged; /* Time an lstat() failure was last logged, 0 if lstat() works */ + time_t last_checked; /* Time of the last successful check, 0 if none yet */ + time_t last_error_logged; /* Time the last error was logged, 0 if no errors */ + + /* What the last successful check observed */ + bool last_exists; } FileWatcherState; -/* Sets *deleted to whether the path of the watcher 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. A persisting failure is logged at most once - * every LSTAT_ERROR_LOG_INTERVAL_SECS. - * - * Doesn't follow symlinks, i.e. a dangling symlink is not considered deleted */ -static bool FileDeleted(FileWatcherState *fws, bool *deleted) + +void *FileWatcherStateNew(const Rlist *args) { - assert(fws != NULL); - assert(deleted != NULL); + assert(args != NULL); + const char *path = RlistScalarValue(args); - struct stat sb; - if (lstat(fws->path, &sb) == 0) - { - *deleted = false; - fws->lstat_error_logged = 0; - return true; - } + FileWatcherState *fws = (FileWatcherState *) xcalloc(1, sizeof(FileWatcherState)); + fws->path = xstrdup(path); - if (errno == ENOENT || errno == ENOTDIR) + return fws; +} + +void FileWatcherStateDestroy(void *state) +{ + FileWatcherState *fws = state; + if (fws != NULL) { - *deleted = true; - fws->lstat_error_logged = 0; - return true; + free(fws->path); + free(fws); } +} + +// =========== code shared by the file event types =========== + +/* Logs the failure of operation op (with errno err) on the file of the + * watcher. A persisting failure is logged at most once every + * CHECK_ERROR_LOG_INTERVAL_SECS. */ +static void LogCheckError(FileWatcherState *fws, const char *op, int err) +{ + assert(fws != NULL); - const int lstat_errno = errno; const time_t now = time(NULL); - if (fws->lstat_error_logged == 0 || - now - fws->lstat_error_logged >= LSTAT_ERROR_LOG_INTERVAL_SECS) + if (fws->last_error_logged == 0 || + now - fws->last_error_logged >= CHECK_ERROR_LOG_INTERVAL_SECS) { - Log(LOG_LEVEL_ERR, "Unable to lstat '%s' while checking for file deletion: %s", - fws->path, GetErrorStrFromCode(lstat_errno)); - fws->lstat_error_logged = now; + Log(LOG_LEVEL_ERR, "Unable to %s '%s' while checking for file events: %s", + op, fws->path, GetErrorStrFromCode(err)); + fws->last_error_logged = now; } - return false; } -void *FileWatcherStateNew(const char *path) +/* Sets *exists to whether the path of the watcher exists, and *sb to its + * lstat() if it does (sb may be NULL). 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 + * exists, so false is returned and it is logged, see LogCheckError(). + * + * Doesn't follow symlinks, i.e. a dangling symlink exists */ +static bool FileLstat(FileWatcherState *fws, bool *exists, struct stat *sb) { - FileWatcherState *fws = (FileWatcherState *) xmalloc(sizeof(FileWatcherState)); + assert(fws != NULL); + assert(exists != NULL); - fws->path = xstrdup(path); - fws->lstat_error_logged = 0; + struct stat buf; + if (lstat(fws->path, (sb != NULL) ? sb : &buf) == 0) + { + *exists = true; + fws->last_error_logged = 0; + return true; + } - /* If the file's presence can't be determined, treat it as not yet seen */ - bool deleted; - fws->last_seen = (FileDeleted(fws, &deleted) && !deleted) ? time(NULL) : 0; + if (errno == ENOENT || errno == ENOTDIR) + { + *exists = false; + fws->last_error_logged = 0; + return true; + } - return (void *) fws; + LogCheckError(fws, "lstat", errno); + return false; } -WatcherCheckResult FileWatcherCheckFileDeleted(void *state) +/* Records a successful check, at which the file existed or not and the event + * (e.g. "deletion") happened or not, and returns its result. The first + * successful check is only the reference for the next ones, so it can't + * fire. */ +static WatcherCheckResult FileWatcherRecordObservation(FileWatcherState *fws, bool exists, + bool happened, const char *event) { - assert(state != NULL); - FileWatcherState *fws = state; - - bool deleted_now; - if (!FileDeleted(fws, &deleted_now)) - { - /* Unknown state, keep last_seen as is */ - return WATCHER_CHECK_ERROR; - } + assert(fws != NULL); + assert(event != NULL); const time_t now = time(NULL); - const bool deleted = (fws->last_seen != 0) && deleted_now; + happened = happened && (fws->last_checked != 0); - if (deleted) + if (happened) { - char last_seen_str[26]; - const char *last_seen_ts = cf_strtimestamp_local(fws->last_seen, last_seen_str); + char last_checked_str[26]; + const char *last_checked_ts = cf_strtimestamp_local(fws->last_checked, last_checked_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)", + "Observed %s of file '%s' at %s (last checked at %s)", + event, fws->path, (now_ts != NULL) ? now_ts : "unknown time", - (last_seen_ts != NULL) ? last_seen_ts : "unknown time"); + (last_checked_ts != NULL) ? last_checked_ts : "unknown time"); } - fws->last_seen = deleted_now ? 0 : now; - return deleted ? WATCHER_CHECK_EVENT : WATCHER_CHECK_NO_EVENT; + fws->last_checked = now; + fws->last_exists = exists; + return happened ? WATCHER_CHECK_EVENT : WATCHER_CHECK_NO_EVENT; } -void DestroyFileWatcherState(void *state) +// =========== code for the file_deleted event =========== + +WatcherCheckResult FileWatcherCheckFileDeleted(void *state) { + assert(state != NULL); FileWatcherState *fws = state; - if (fws != NULL) + + bool exists; + if (!FileLstat(fws, &exists, NULL)) { - free(fws->path); - free(fws); + /* Unknown state, keep the last observation as is */ + return WATCHER_CHECK_ERROR; } + + bool deleted = fws->last_exists && !exists; + return FileWatcherRecordObservation(fws, exists, deleted, "deletion"); } diff --git a/cf-reactor/file_watcher.h b/cf-reactor/file_watcher.h index f96ea8daae5..2ab0eaa5cd5 100644 --- a/cf-reactor/file_watcher.h +++ b/cf-reactor/file_watcher.h @@ -29,8 +29,8 @@ #include /* WatcherCheckResult */ -void *FileWatcherStateNew(const char *path); +void *FileWatcherStateNew(const Rlist *args); WatcherCheckResult FileWatcherCheckFileDeleted(void *state); -void DestroyFileWatcherState(void *state); +void FileWatcherStateDestroy(void *state); #endif diff --git a/cf-reactor/reactor_transform.c b/cf-reactor/reactor_transform.c index 27c725a46fb..854de5606d8 100644 --- a/cf-reactor/reactor_transform.c +++ b/cf-reactor/reactor_transform.c @@ -33,13 +33,16 @@ #include #include #include -#include #include // ScheduleAgentOperations() #include // GetClassContextAttributes() #include // ConnCache_Init(), ConnCache_Destroy() #include // Initialize/FinalizeCustomPromises() #include // SetReferenceTime() #include // UpdateTimeClasses() +#include +#include // PromiseTypeSyntaxGet(), BodySyntaxGetConstraintSyntax() +#include +#include /* Promise types evaluated within `bundle reactor NAME { ... }`. */ static const char *const REACTOR_TYPESEQUENCE[] = @@ -98,18 +101,6 @@ static const Bundle *ResolveThenBundle( return bp; } -/* Records the outcome of an iteration of an events promise: defines the - * classes of its 'classes' body, and logs it like cf-agent does for its - * promises. */ -static void RecordEventsPromiseOutcome(EvalContext *ctx, const Promise *pp, PromiseResult result) -{ - assert(pp != NULL); - - Attributes a = GetClassContextAttributes(ctx, pp); - ClassAuditLog(ctx, pp, &a, result); - EvalContextLogPromiseIterationOutcome(ctx, pp, result); -} - /* Returns the list of bundle references of the (expanded) 'then' attribute, * which is always a list after expansion, see ExpandBundleListReference(). * Logs an error and returns NULL if there is no 'then' attribute or it is @@ -134,22 +125,145 @@ static const Rlist *GetThenBundles(const Promise *pp) return bundles; } -// Temporary limitations: -// - 'when' body: only a single constraint, 'file_deleted', is supported (no OR-ing of constraints yet) -// TODO: support multiple 'when' constraints -static PromiseResult KeepEventsPromise(EvalContext *ctx, const Promise *pp) +/*****************************************************************************/ + +typedef struct +{ + const char *name; + const char *const *extra_args; + WatcherStateNewFn new_state; + WatcherStateDestroyFn destroy_state; + WatcherCheckFn check; +} EventType; + +static const char *const NO_EXTRA_ARGS[] = +{ + NULL +}; + +#define EventTypeNew(name, args, new_state, destroy_state, check) \ + { name, args, new_state, destroy_state, check } +#define EventTypeNewNull() EventTypeNew(NULL, NULL, NULL, NULL, NULL) + +/* A file event, about the file at the path of its constraint */ +#define FileEventTypeNew(name, check) \ + EventTypeNew(name, NO_EXTRA_ARGS, FileWatcherStateNew, FileWatcherStateDestroy, check) + +static const EventType EVENT_TABLE[] = +{ + FileEventTypeNew(REACTOR_FILE_DELETED, FileWatcherCheckFileDeleted), + EventTypeNewNull() +}; + +static const EventType *EventsPromiseGetFirstEvent(const Promise *pp, Rlist **args) { assert(pp != NULL); + assert(args != NULL); - const char *path = PromiseGetConstraintAsRval(pp, "file_deleted", RVAL_TYPE_SCALAR); - if (path == NULL) + for (size_t i = 0; EVENT_TABLE[i].name != NULL; i++) { - Log(LOG_LEVEL_WARNING, - "Reactor events promise '%s' must specify exactly one file to watch in its 'when' body, ignoring", - pp->promiser); - return PROMISE_RESULT_FAIL; + const Constraint *con = PromiseGetConstraint(pp, EVENT_TABLE[i].name); + if (con != NULL) + { + RlistAppend(args, con->rval.item, con->rval.type); + return &EVENT_TABLE[i]; + } + } + Log(LOG_LEVEL_ERR, + "Reactor events promise '%s' does not specify an event to watch in its 'when' body", + pp->promiser); + return NULL; +} + +static time_t DEFAULT_INTERVAL = 1; + +static time_t EventsPromiseGetInterval(const Promise *pp) +{ + const Constraint *interval_con = PromiseGetConstraint(pp, "interval"); + if (interval_con == NULL) + { + return DEFAULT_INTERVAL; + } + return (time_t) IntFromString((char *) interval_con->rval.item); +} + +static bool ResolveWhenBody(const Promise *pp, WatcherOptions *opt) +{ + assert(pp != NULL); + assert(opt != NULL); + // 1. Get the event and its arg + Rlist *args = NULL; + const EventType *event = EventsPromiseGetFirstEvent(pp, &args); + + if (event == NULL) + { + RlistDestroy(args); + return false; } + // 2. if the event has additional args, add them to the list of args + // TODO: default values? + for (const char *const *extra_arg = event->extra_args; *extra_arg != NULL; extra_arg++) + { + const Constraint *con = PromiseGetConstraint(pp, *extra_arg); + if (con == NULL) + { + Log(LOG_LEVEL_ERR, + "Constraint '%s' is required for event '%s' in events promise '%s'", + *extra_arg, event->name, pp->promiser); + RlistDestroy(args); + return false; + } + RlistAppend(&args, con->rval.item, con->rval.type); + } + + // 3. check that other constraints are not defined in the promise, that + // do not belong here + const PromiseTypeSyntax *events_syntax = PromiseTypeSyntaxGet("reactor", "events"); + assert(events_syntax != NULL); + const ConstraintSyntax *when_syntax = + PromiseTypeSyntaxGetConstraintSyntax(events_syntax, "when")->range.body_type_syntax->constraints; + + for (size_t i = 0; i < SeqLength(pp->conlist); i++) + { + const Constraint *cp = SeqAt(pp->conlist, i); + if (BodySyntaxGetConstraintSyntax(when_syntax, cp->lval) == NULL || /* not a 'when' constraint */ + StringEqual(cp->lval, event->name) || + StringEqual(cp->lval, "interval") || + StringEqual(cp->lval, "meta")) /* also a promise attribute */ + { + continue; + } + + bool own_arg = false; + for (const char *const *extra_arg = event->extra_args; *extra_arg != NULL; extra_arg++) + { + own_arg = own_arg || StringEqual(*extra_arg, cp->lval); + } + if (!own_arg) + { + Log(LOG_LEVEL_ERR, + "Running mutually exclusive constraints for '%s' event type in '%s' events promise", + event->name, pp->promiser); + RlistDestroy(args); + return false; + } + } + + opt->state = event->new_state(args); + opt->check_callback = event->check; + opt->destroy_state = event->destroy_state; + opt->interval = EventsPromiseGetInterval(pp); + RlistDestroy(args); + return true; +} + +/*****************************************************************************/ + +static PromiseResult KeepEventsPromise(EvalContext *ctx, const Promise *pp) +{ + assert(pp != NULL); + const Rlist *then_bundles = GetThenBundles(pp); if (then_bundles == NULL) { @@ -164,19 +278,33 @@ static PromiseResult KeepEventsPromise(EvalContext *ctx, const Promise *pp) } } - WatcherOptions opt = { - .state = FileWatcherStateNew(path), - .check_callback = FileWatcherCheckFileDeleted, - .destroy_state = DestroyFileWatcherState, - }; + WatcherOptions opt; + if (!ResolveWhenBody(pp, &opt)) + { + return PROMISE_RESULT_FAIL; + } - // register watcher - Log(LOG_LEVEL_INFO, "Registering a file_deleted watcher for events promise '%s', on file '%s'", pp->promiser, path); - WatcherRegister(pp, opt, 1); + bool failed = WatcherRegister(pp, &opt); + if (failed) + { + return PROMISE_RESULT_FAIL; + } return PROMISE_RESULT_NOOP; } +/* Records the outcome of an iteration of an events promise: defines the + * classes of its 'classes' body, and logs it like cf-agent does for its + * promises. */ +static void RecordEventsPromiseOutcome(EvalContext *ctx, const Promise *pp, PromiseResult result) +{ + assert(pp != NULL); + + Attributes a = GetClassContextAttributes(ctx, pp); + ClassAuditLog(ctx, pp, &a, result); + EvalContextLogPromiseIterationOutcome(ctx, pp, result); +} + static PromiseResult KeepReactorPromise(EvalContext *ctx, const Promise *pp, ARG_UNUSED void *param) { assert(param == NULL); diff --git a/cf-reactor/watcher.c b/cf-reactor/watcher.c index 2fee123a574..bd16a150f62 100644 --- a/cf-reactor/watcher.c +++ b/cf-reactor/watcher.c @@ -118,15 +118,15 @@ void WatcherRegistryFinalize(void) } // Expects interval to be strictly greater than 0, otherwise the watcher thread will busy spin -void WatcherRegister(const Promise *pp, WatcherOptions opt, time_t interval) +bool WatcherRegister(const Promise *pp, WatcherOptions *opt) { assert(pp != NULL); assert(pp->promiser != NULL); - assert(opt.check_callback != NULL); - assert(opt.destroy_state != NULL); - assert(opt.state != NULL); + assert(opt->check_callback != NULL); + assert(opt->destroy_state != NULL); + assert(opt->state != NULL); + assert(opt->interval > 0); assert(watchers != NULL); - assert(interval > 0); ThreadLock(&watchers_mutex); char promise_hash[CF_HOSTKEY_STRING_SIZE]; @@ -137,37 +137,41 @@ void WatcherRegister(const Promise *pp, WatcherOptions opt, time_t interval) { // ignore duplicates ThreadUnlock(&watchers_mutex); - opt.destroy_state(opt.state); - return; + opt->destroy_state(opt->state); + return false; } Watcher *w = xmalloc(sizeof(Watcher)); w->promise_hash = xstrdup(promise_hash); w->promise = pp->org_pp; w->promiser = xstrdup(pp->promiser); - w->poll_interval_secs = interval; + w->poll_interval_secs = opt->interval; w->next_due = 0; /* due immediately on the watcher thread's first pass */ w->last_check = WATCHER_CHECK_NO_EVENT; - w->check_callback = opt.check_callback; - w->destroy_state = opt.destroy_state; + w->check_callback = opt->check_callback; + w->destroy_state = opt->destroy_state; // Take over the state of an identical watcher + bool failed = false; prev = (old_watchers != NULL) ? MapGet(old_watchers, promise_hash) : NULL; if (prev == NULL) { - w->state = opt.state; + w->state = opt->state; + WatcherCheckResult check = w->check_callback(w->state); + failed = (check == WATCHER_CHECK_ERROR); } else { Log(LOG_LEVEL_DEBUG, "Reactor watcher '%s' takes over the state of an identical watcher", w->promiser); w->state = prev->state; prev->state = NULL; /* so not destroyed with prev */ - opt.destroy_state(opt.state); + opt->destroy_state(opt->state); } MapInsert(watchers, w->promise_hash, w); ThreadUnlock(&watchers_mutex); + return failed; } static void WatcherDestroy(void *item) @@ -292,7 +296,7 @@ static void HandleQueuedEvents(EvalContext *ctx, WatcherEventFn on_event) } else { - Log(LOG_LEVEL_NOTICE, "Reactor watcher '%s' fired", w->promiser); + Log(LOG_LEVEL_VERBOSE, "Reactor watcher '%s' fired", w->promiser); } on_event(ctx, w->promise, w->promise_hash, event->check); free(event); diff --git a/cf-reactor/watcher.h b/cf-reactor/watcher.h index 58c66a62ba5..452404471a6 100644 --- a/cf-reactor/watcher.h +++ b/cf-reactor/watcher.h @@ -35,6 +35,7 @@ typedef enum WATCHER_CHECK_ERROR, /* unable to tell whether the event happened */ } WatcherCheckResult; +typedef void *(*WatcherStateNewFn)(const Rlist *args); typedef WatcherCheckResult (*WatcherCheckFn)(void *state); typedef void (*WatcherStateDestroyFn)(void *state); @@ -46,6 +47,7 @@ typedef struct WatcherCheckFn check_callback; /* whether the event happened, updating state */ WatcherStateDestroyFn destroy_state; void *state; /* owned by the registry once registered */ + time_t interval; /* seconds between two checks, > 0 */ } WatcherOptions; /** @@ -85,18 +87,21 @@ void WatcherRegistryFinalize(void); * * If a watcher of the previous policy (see EventWatcherPause()) was * registered for an identical expanded promise, the new watcher takes over - * its state instead of opt.state, so that the events happening while paused - * are detected. + * its state instead of opt->state, so that the events happening while paused + * are detected. Otherwise opt->state is checked once right away, as the + * reference for the next checks, so that only what happens from now on + * fires. * * @param pp the expanded events promise. Its hash and its unexpanded * promise (pp->org_pp) are passed back to the WatcherEventFn on * event. The unexpanded promise is not owned, so the policy * holding it must not be destroyed before EventWatcherPause(). - * @param opt what the watcher checks for, see WatcherOptions. opt.state is - * owned by the registry. - * @param interval interval between runs + * @param opt what the watcher checks for, see WatcherOptions. opt->state is + * owned by the registry, which destroys it right away if not + * used, so it must not be used by the caller afterwards. + * opt->interval must be strictly greater than 0. */ -void WatcherRegister(const Promise *pp, WatcherOptions opt, time_t interval); +bool WatcherRegister(const Promise *pp, WatcherOptions *opt); bool EventWatcherInitialize(int *fd); void EventWatcherHandleEvents(EvalContext *ctx, WatcherEventFn on_event, int fd, fd_set *readfds); diff --git a/libpromises/mod_reactor.c b/libpromises/mod_reactor.c index 113a3e9a1a8..51610f86f11 100644 --- a/libpromises/mod_reactor.c +++ b/libpromises/mod_reactor.c @@ -27,12 +27,16 @@ #include #include +#define CF_NZERO_VALRANGE "1,99999999999" + static const ConstraintSyntax when_constraints[] = { CONSTRAINT_SYNTAX_GLOBAL, /* Row models */ - ConstraintSyntaxNewString("file_deleted", CF_ANYSTRING, "File to react for on deletion", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_DELETED, CF_ANYSTRING, "File to react for on deletion", SYNTAX_STATUS_NORMAL), + /* default */ + ConstraintSyntaxNewInt("interval", CF_NZERO_VALRANGE, "Seconds between two checks for the event", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() }; diff --git a/libpromises/mod_reactor.h b/libpromises/mod_reactor.h index 72842f35045..b26d3d1f93a 100644 --- a/libpromises/mod_reactor.h +++ b/libpromises/mod_reactor.h @@ -27,6 +27,8 @@ #include +#define REACTOR_FILE_DELETED "file_deleted" + extern const PromiseTypeSyntax CF_REACTOR_PROMISE_TYPES[]; #endif From 3f5cbb2ba5a4fc90d5fc18dcc2b5fb1353f6b885 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 14 Oct 2026 16:49:26 +0200 Subject: [PATCH 2/3] Adapt watcher unit tests to the event types interface Signed-off-by: Victor Moene --- tests/unit/watcher_test.c | 73 ++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/tests/unit/watcher_test.c b/tests/unit/watcher_test.c index 365ce620c62..ac5eea9f392 100644 --- a/tests/unit/watcher_test.c +++ b/tests/unit/watcher_test.c @@ -3,6 +3,7 @@ #include #include /* FileWatcherStateNew(), FileWatcherCheckFileDeleted() */ #include /* Promise */ +#include /* RlistAppendScalar() */ #include /* LoggingPrivContext, LoggingPrivSetContext() */ #include /* strstr() */ @@ -34,14 +35,22 @@ static const Promise expanded_promise = { .org_pp = &unexpanded_promise, }; -static WatcherOptions FileDeletedOptions(const char *path) +/* Returns the options of a file_deleted watcher, valid until the next call + * (WatcherRegister() copies them) */ +static WatcherOptions *FileDeletedOptions(const char *path, time_t interval) { - WatcherOptions opt = { - .state = FileWatcherStateNew(path), + static WatcherOptions opt; + + Rlist *args = NULL; + RlistAppendScalar(&args, path); + opt = (WatcherOptions) { + .state = FileWatcherStateNew(args), .check_callback = FileWatcherCheckFileDeleted, - .destroy_state = DestroyFileWatcherState, + .destroy_state = FileWatcherStateDestroy, + .interval = interval, }; - return opt; + RlistDestroy(args); + return &opt; } /* Creates the file 'file' in a new temporary directory dir */ @@ -121,7 +130,7 @@ static void test_watcher_register_single(void) { WatcherRegistryInitialize(); - WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event"), 5); + WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event", 5)); WatcherRegistryFinalize(); } @@ -134,8 +143,8 @@ static void test_watcher_register_iterations(void) * different watchers */ const Promise iteration_a = { .promiser = "/a", .org_pp = &unexpanded_promise }; const Promise iteration_b = { .promiser = "/b", .org_pp = &unexpanded_promise }; - WatcherRegister(&iteration_a, FileDeletedOptions("/nonexistent/a"), 5); - WatcherRegister(&iteration_b, FileDeletedOptions("/nonexistent/b"), 5); + WatcherRegister(&iteration_a, FileDeletedOptions("/nonexistent/a", 5)); + WatcherRegister(&iteration_b, FileDeletedOptions("/nonexistent/b", 5)); WatcherRegistryFinalize(); } @@ -157,8 +166,8 @@ static void test_watcher_register_identical_ignored(void) .promiser = "test-event", .org_pp = &other_unexpanded_promise, }; - WatcherRegister(&expanded_promise, FileDeletedOptions(path), 1); - WatcherRegister(&other_expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&expanded_promise, FileDeletedOptions(path, 1)); + WatcherRegister(&other_expanded_promise, FileDeletedOptions(path, 1)); int fd = -1; assert_true(EventWatcherInitialize(&fd)); @@ -185,18 +194,18 @@ static void test_watcher_register_again_after_pause(void) * registered again for the new policy, also without the event watcher * being initialized */ ResetRecordedEvents(); - WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event"), 5); + WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event", 5)); EventWatcherPause(NULL, RecordEvent); - WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event"), 5); + WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event", 5)); EventWatcherResume(); /* Paused twice in a row (e.g. a failed policy reload in between), the * watchers set aside the first time are replaced by those registered * since, without leaking (checked by valgrind/ASan) */ EventWatcherPause(NULL, RecordEvent); - WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event"), 5); + WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event", 5)); EventWatcherPause(NULL, RecordEvent); - WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event"), 5); + WatcherRegister(&expanded_promise, FileDeletedOptions("/nonexistent/test-event", 5)); EventWatcherResume(); assert_int_equal(handled_event_count, 0); @@ -238,7 +247,7 @@ static void test_event_watcher_pause_handles_queued_events(void) CreateTempFile(dir, path, sizeof(path)); WatcherRegistryInitialize(); - WatcherRegister(&expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&expanded_promise, FileDeletedOptions(path, 1)); int fd = -1; assert_true(EventWatcherInitialize(&fd)); @@ -270,7 +279,7 @@ static void test_event_watcher_state_kept_across_pause(void) CreateTempFile(dir, path, sizeof(path)); WatcherRegistryInitialize(); - WatcherRegister(&expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&expanded_promise, FileDeletedOptions(path, 1)); int fd = -1; assert_true(EventWatcherInitialize(&fd)); @@ -290,7 +299,7 @@ static void test_event_watcher_state_kept_across_pause(void) .promiser = "test-event", .org_pp = &new_unexpanded_promise, }; - WatcherRegister(&new_expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&new_expanded_promise, FileDeletedOptions(path, 1)); EventWatcherResume(); assert_true(WaitForQueuedEvent(fd, 50)); @@ -311,7 +320,7 @@ static void test_event_watcher_state_kept_identical_ignored(void) CreateTempFile(dir, path, sizeof(path)); WatcherRegistryInitialize(); - WatcherRegister(&expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&expanded_promise, FileDeletedOptions(path, 1)); int fd = -1; assert_true(EventWatcherInitialize(&fd)); @@ -333,8 +342,8 @@ static void test_event_watcher_state_kept_identical_ignored(void) .promiser = "test-event", .org_pp = &other_unexpanded_promise, }; - WatcherRegister(&new_expanded_promise, FileDeletedOptions(path), 1); - WatcherRegister(&other_expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&new_expanded_promise, FileDeletedOptions(path, 1)); + WatcherRegister(&other_expanded_promise, FileDeletedOptions(path, 1)); EventWatcherResume(); assert_true(WaitForQueuedEvent(fd, 50)); @@ -354,7 +363,7 @@ static void test_event_watcher_state_not_kept_for_other_promise(void) CreateTempFile(dir, path, sizeof(path)); WatcherRegistryInitialize(); - WatcherRegister(&expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&expanded_promise, FileDeletedOptions(path, 1)); int fd = -1; assert_true(EventWatcherInitialize(&fd)); @@ -369,7 +378,7 @@ static void test_event_watcher_state_not_kept_for_other_promise(void) .promiser = "other-event", .org_pp = &unexpanded_promise, }; - WatcherRegister(&other_expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&other_expanded_promise, FileDeletedOptions(path, 1)); EventWatcherResume(); /* The watcher thread checks at least every second */ @@ -407,9 +416,12 @@ static void test_check_file_deleted_error(void) const LogLevel old_level = LogGetGlobalLevel(); LogSetGlobalLevel(LOG_LEVEL_CRIT); - void *state = FileWatcherStateNew(path); + Rlist *args = NULL; + RlistAppendScalar(&args, path); + void *state = FileWatcherStateNew(args); + RlistDestroy(args); assert_int_equal(FileWatcherCheckFileDeleted(state), WATCHER_CHECK_ERROR); - DestroyFileWatcherState(state); + FileWatcherStateDestroy(state); LogSetGlobalLevel(old_level); TearDownSymlinkLoop(dir, loop); @@ -432,8 +444,13 @@ static void test_check_file_deleted_logs_error_once(void) const LogLevel old_level = LogGetGlobalLevel(); LogSetGlobalLevel(LOG_LEVEL_CRIT); - /* The failure at registration is logged */ - void *state = FileWatcherStateNew(path); + /* The failure of the first check is logged */ + Rlist *args = NULL; + RlistAppendScalar(&args, path); + void *state = FileWatcherStateNew(args); + RlistDestroy(args); + assert_int_equal(captured_err_count, 0); + assert_int_equal(FileWatcherCheckFileDeleted(state), WATCHER_CHECK_ERROR); assert_int_equal(captured_err_count, 1); assert_true(strstr(captured_err_message, "Unable to lstat") != NULL); @@ -451,7 +468,7 @@ static void test_check_file_deleted_logs_error_once(void) assert_int_equal(FileWatcherCheckFileDeleted(state), WATCHER_CHECK_ERROR); assert_int_equal(captured_err_count, 2); - DestroyFileWatcherState(state); + FileWatcherStateDestroy(state); LogSetGlobalLevel(old_level); LoggingPrivSetContext(NULL); @@ -469,7 +486,7 @@ static void test_event_watcher_check_failure_reported_once(void) LogSetGlobalLevel(LOG_LEVEL_CRIT); WatcherRegistryInitialize(); - WatcherRegister(&expanded_promise, FileDeletedOptions(path), 1); + WatcherRegister(&expanded_promise, FileDeletedOptions(path, 1)); int fd = -1; assert_true(EventWatcherInitialize(&fd)); From 9fef6bf571a582de55d1ad46522711ece0fe95de Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Wed, 14 Oct 2026 16:56:09 +0200 Subject: [PATCH 3/3] Added file event types 1. file_created: the file appears at the path. 2. file_modified: the modification time of the file changes. 3. file_content_modified: the SHA-256 hash of a regular file's content changes. 4. file_permissions_changed: the permission bits (07777) change. 5. file_owner_changed: the owning user or group changes. 6. file_replaced: the path now points to a different file (another inode or device), for example after an atomic rename over it. 7. file_type_changed: the file's type changes, for example a regular file replaced by a directory or a symlink. 8. symlink_target_changed: the target of a symlink changes. 9. file_size_exceeds: the size grows above size_threshold bytes. This requires the extra size_threshold constraint. Ticket: ENT-14692 Signed-off-by: Victor Moene --- cf-reactor/file_watcher.c | 368 +++++++++++++++++++++++++++++++++ cf-reactor/file_watcher.h | 13 +- cf-reactor/reactor_transform.c | 16 ++ libpromises/mod_reactor.c | 10 + libpromises/mod_reactor.h | 10 + 5 files changed, 416 insertions(+), 1 deletion(-) diff --git a/cf-reactor/file_watcher.c b/cf-reactor/file_watcher.c index 1fec26adf37..c68cbaeb0b4 100644 --- a/cf-reactor/file_watcher.c +++ b/cf-reactor/file_watcher.c @@ -26,14 +26,21 @@ #include #include #include +#include /* safe_fopen() */ +#include /* HashDigestFromId() */ +#include /* StringEqual() */ +#include /* IntFromString() */ #include /* cf_strtimestamp_local() */ #include #include +#include /* Minimum time between two logs of a persisting failure to check the file, * since the file is checked on every poll */ #define CHECK_ERROR_LOG_INTERVAL_SECS 60 +#define CONTENT_HASH_METHOD HASH_METHOD_SHA256 + typedef struct { char *path; @@ -42,6 +49,49 @@ typedef struct /* What the last successful check observed */ bool last_exists; + + /* What the last successful check observed, depending on the event type */ + union + { + /* FileWatcherCheckFileModified() */ + struct timespec mtime; + + /* FileWatcherCheckFileContentModified() */ + struct + { + bool hashed; /* Whether it was a regular file, that was hashed */ + unsigned char digest[EVP_MAX_MD_SIZE + 1]; + } content; + + /* FileWatcherCheckFilePermissionsChanged(), FileWatcherCheckFileTypeChanged() */ + mode_t mode; + + /* FileWatcherCheckFileOwnerChanged() */ + struct + { + uid_t uid; + gid_t gid; + } owner; + + /* FileWatcherCheckFileReplaced() */ + struct + { + dev_t dev; + ino_t ino; + } inode; + + /* FileWatcherCheckFileSizeExceeds() */ + struct + { + off_t threshold; /* Not observed, set when the state is created */ + off_t size; + } file_size; + } last; + + /* What the last successful check of FileWatcherCheckSymlinkTargetChanged() + * observed, NULL if not a symlink. Not in the union above, so that + * FileWatcherStateDestroy() can free it for every event type */ + char *last_target; } FileWatcherState; @@ -56,12 +106,26 @@ void *FileWatcherStateNew(const Rlist *args) return fws; } +void *FileWatcherSizeStateNew(const Rlist *args) +{ + assert(args != NULL && args->next != NULL); + const char *path = RlistScalarValue(args); + const long size_threshold = IntFromString(RlistScalarValue(args->next)); + + FileWatcherState *fws = (FileWatcherState *) xcalloc(1, sizeof(FileWatcherState)); + fws->path = xstrdup(path); + fws->last.file_size.threshold = size_threshold; + + return fws; +} + void FileWatcherStateDestroy(void *state) { FileWatcherState *fws = state; if (fws != NULL) { free(fws->path); + free(fws->last_target); free(fws); } } @@ -168,3 +232,307 @@ WatcherCheckResult FileWatcherCheckFileDeleted(void *state) return FileWatcherRecordObservation(fws, exists, deleted, "deletion"); } +// =========== code for the file_created event =========== + +WatcherCheckResult FileWatcherCheckFileCreated(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + if (!FileLstat(fws, &exists, NULL)) + { + return WATCHER_CHECK_ERROR; + } + + const bool created = !fws->last_exists && exists; + return FileWatcherRecordObservation(fws, exists, created, "creation"); +} + +// =========== code for the file_modified event =========== + +static struct timespec StatMtime(const struct stat *sb) +{ + assert(sb != NULL); + +#if defined(HAVE_STRUCT_STAT_ST_MTIM) + return sb->st_mtim; +#elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC) + return sb->st_mtimespec; +#else + return (struct timespec) { .tv_sec = sb->st_mtime, .tv_nsec = 0 }; +#endif +} + +WatcherCheckResult FileWatcherCheckFileModified(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + struct stat sb; + if (!FileLstat(fws, &exists, &sb)) + { + return WATCHER_CHECK_ERROR; + } + + const struct timespec mtime = exists ? StatMtime(&sb) : (struct timespec) { 0 }; + + /* Only modifications of a file present at both checks */ + const bool modified = fws->last_exists && exists && + ((fws->last.mtime.tv_sec != mtime.tv_sec) || + (fws->last.mtime.tv_nsec != mtime.tv_nsec)); + + fws->last.mtime = mtime; + return FileWatcherRecordObservation(fws, exists, modified, "modification"); +} + +// =========== code for the file_content_modified event =========== + +/* 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]) +{ + assert(path != NULL); + + FILE *file = safe_fopen(path, "rb"); + if (file == NULL) + { + return false; + } + + EVP_MD_CTX *context = EVP_MD_CTX_create(); + if (context == NULL || + EVP_DigestInit_ex(context, HashDigestFromId(CONTENT_HASH_METHOD), NULL) != 1) + { + EVP_MD_CTX_destroy(context); + fclose(file); + errno = ENOMEM; + return false; + } + + unsigned char buffer[4096]; + size_t read_count; + while ((read_count = fread(buffer, 1, sizeof(buffer), file)) > 0) + { + EVP_DigestUpdate(context, buffer, read_count); + } + + const bool success = !ferror(file); + const int read_errno = errno; + fclose(file); + + memset(digest, 0, EVP_MAX_MD_SIZE + 1); + EVP_DigestFinal_ex(context, digest, NULL); + EVP_MD_CTX_destroy(context); + + errno = read_errno; + return success; +} + +WatcherCheckResult FileWatcherCheckFileContentModified(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + struct stat sb; + if (!FileLstat(fws, &exists, &sb)) + { + return WATCHER_CHECK_ERROR; + } + + bool hashed = false; + unsigned char digest[EVP_MAX_MD_SIZE + 1] = { 0 }; + if (exists && S_ISREG(sb.st_mode)) + { + if (HashFileContent(fws->path, digest)) + { + hashed = true; + } + else if (errno != ENOENT && errno != ENOTDIR) /* Not deleted since lstat() */ + { + LogCheckError(fws, "hash", errno); + return WATCHER_CHECK_ERROR; + } + } + + /* Only content changes of a regular file present at both checks */ + const bool modified = fws->last.content.hashed && hashed && + (memcmp(fws->last.content.digest, digest, sizeof(digest)) != 0); + + fws->last.content.hashed = hashed; + memcpy(fws->last.content.digest, digest, sizeof(digest)); + return FileWatcherRecordObservation(fws, exists, modified, "content modification"); +} + +// =========== code for the file_permissions_changed event =========== + +WatcherCheckResult FileWatcherCheckFilePermissionsChanged(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + struct stat sb; + if (!FileLstat(fws, &exists, &sb)) + { + return WATCHER_CHECK_ERROR; + } + + const mode_t mode = exists ? sb.st_mode : 0; + + /* Only changes of a file present at both checks */ + const bool changed = fws->last_exists && exists && + ((fws->last.mode & 07777) != (mode & 07777)); + + fws->last.mode = mode; + return FileWatcherRecordObservation(fws, exists, changed, "permissions change"); +} + +// =========== code for the file_owner_changed event =========== + +WatcherCheckResult FileWatcherCheckFileOwnerChanged(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + struct stat sb; + if (!FileLstat(fws, &exists, &sb)) + { + return WATCHER_CHECK_ERROR; + } + + const uid_t uid = exists ? sb.st_uid : 0; + const gid_t gid = exists ? sb.st_gid : 0; + + /* Only changes of a file present at both checks */ + const bool changed = fws->last_exists && exists && + ((fws->last.owner.uid != uid) || (fws->last.owner.gid != gid)); + + fws->last.owner.uid = uid; + fws->last.owner.gid = gid; + return FileWatcherRecordObservation(fws, exists, changed, "owner change"); +} + +// =========== code for the file_replaced event =========== + +WatcherCheckResult FileWatcherCheckFileReplaced(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + struct stat sb; + if (!FileLstat(fws, &exists, &sb)) + { + return WATCHER_CHECK_ERROR; + } + + const dev_t dev = exists ? sb.st_dev : 0; + const ino_t ino = exists ? sb.st_ino : 0; + + /* Only a path present at both checks, referring to another file (e.g. an + * atomic rename over it) */ + const bool replaced = fws->last_exists && exists && + ((fws->last.inode.dev != dev) || (fws->last.inode.ino != ino)); + + fws->last.inode.dev = dev; + fws->last.inode.ino = ino; + return FileWatcherRecordObservation(fws, exists, replaced, "replacement"); +} + +// =========== code for the file_type_changed event =========== + +WatcherCheckResult FileWatcherCheckFileTypeChanged(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + struct stat sb; + if (!FileLstat(fws, &exists, &sb)) + { + return WATCHER_CHECK_ERROR; + } + + const mode_t mode = exists ? sb.st_mode : 0; + + /* Only changes of a path present at both checks, e.g. a file replaced by + * a symlink */ + const bool changed = fws->last_exists && exists && + ((fws->last.mode & S_IFMT) != (mode & S_IFMT)); + + fws->last.mode = mode; + return FileWatcherRecordObservation(fws, exists, changed, "type change"); +} + +// =========== code for the symlink_target_changed event =========== + +WatcherCheckResult FileWatcherCheckSymlinkTargetChanged(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + struct stat sb; + if (!FileLstat(fws, &exists, &sb)) + { + return WATCHER_CHECK_ERROR; + } + + char *target = NULL; +#ifndef __MINGW32__ + if (exists && S_ISLNK(sb.st_mode)) + { + char buf[PATH_MAX]; + const ssize_t len = readlink(fws->path, buf, sizeof(buf) - 1); + if (len >= 0) + { + buf[len] = '\0'; + target = xstrdup(buf); + } + /* Not deleted (ENOENT, ENOTDIR) or replaced by another type of file + * (EINVAL) since lstat() */ + else if (errno != ENOENT && errno != ENOTDIR && errno != EINVAL) + { + LogCheckError(fws, "readlink", errno); + return WATCHER_CHECK_ERROR; + } + } +#endif + + /* Only target changes of a symlink present at both checks */ + const bool changed = (fws->last_target != NULL) && (target != NULL) && + !StringEqual(fws->last_target, target); + + free(fws->last_target); + fws->last_target = target; + return FileWatcherRecordObservation(fws, exists, changed, "symlink target change"); +} + +// =========== code for the file_size_exceeds event =========== + +WatcherCheckResult FileWatcherCheckFileSizeExceeds(void *state) +{ + assert(state != NULL); + FileWatcherState *fws = state; + + bool exists; + struct stat sb; + if (!FileLstat(fws, &exists, &sb)) + { + return WATCHER_CHECK_ERROR; + } + + const off_t size = exists ? sb.st_size : 0; + + /* Only when the size grows above the threshold, including a file created + * above it, not every time it is checked above it */ + const bool exceeded = fws->last_exists && (fws->last.file_size.size > fws->last.file_size.threshold); + const bool exceeds = exists && (size > fws->last.file_size.threshold); + + fws->last.file_size.size = size; + return FileWatcherRecordObservation(fws, exists, !exceeded && exceeds, "size exceeding threshold"); +} diff --git a/cf-reactor/file_watcher.h b/cf-reactor/file_watcher.h index 2ab0eaa5cd5..1be51b9bcde 100644 --- a/cf-reactor/file_watcher.h +++ b/cf-reactor/file_watcher.h @@ -30,7 +30,18 @@ void *FileWatcherStateNew(const Rlist *args); -WatcherCheckResult FileWatcherCheckFileDeleted(void *state); +void *FileWatcherSizeStateNew(const Rlist *args); void FileWatcherStateDestroy(void *state); +WatcherCheckResult FileWatcherCheckFileDeleted(void *state); +WatcherCheckResult FileWatcherCheckFileCreated(void *state); +WatcherCheckResult FileWatcherCheckFileModified(void *state); +WatcherCheckResult FileWatcherCheckFileContentModified(void *state); +WatcherCheckResult FileWatcherCheckFilePermissionsChanged(void *state); +WatcherCheckResult FileWatcherCheckFileOwnerChanged(void *state); +WatcherCheckResult FileWatcherCheckFileReplaced(void *state); +WatcherCheckResult FileWatcherCheckFileTypeChanged(void *state); +WatcherCheckResult FileWatcherCheckSymlinkTargetChanged(void *state); +WatcherCheckResult FileWatcherCheckFileSizeExceeds(void *state); + #endif diff --git a/cf-reactor/reactor_transform.c b/cf-reactor/reactor_transform.c index 854de5606d8..6057f43d1ec 100644 --- a/cf-reactor/reactor_transform.c +++ b/cf-reactor/reactor_transform.c @@ -141,6 +141,12 @@ static const char *const NO_EXTRA_ARGS[] = NULL }; +static const char *const FILE_SIZE_EXCEEDS_ARGS[] = +{ + REACTOR_SIZE_THRESHOLD, + NULL +}; + #define EventTypeNew(name, args, new_state, destroy_state, check) \ { name, args, new_state, destroy_state, check } #define EventTypeNewNull() EventTypeNew(NULL, NULL, NULL, NULL, NULL) @@ -152,6 +158,16 @@ static const char *const NO_EXTRA_ARGS[] = static const EventType EVENT_TABLE[] = { FileEventTypeNew(REACTOR_FILE_DELETED, FileWatcherCheckFileDeleted), + FileEventTypeNew(REACTOR_FILE_CREATED, FileWatcherCheckFileCreated), + FileEventTypeNew(REACTOR_FILE_MODIFIED, FileWatcherCheckFileModified), + FileEventTypeNew(REACTOR_FILE_CONTENT_MODIFIED, FileWatcherCheckFileContentModified), + FileEventTypeNew(REACTOR_FILE_PERMISSIONS_CHANGED, FileWatcherCheckFilePermissionsChanged), + FileEventTypeNew(REACTOR_FILE_OWNER_CHANGED, FileWatcherCheckFileOwnerChanged), + FileEventTypeNew(REACTOR_FILE_REPLACED, FileWatcherCheckFileReplaced), + FileEventTypeNew(REACTOR_FILE_TYPE_CHANGED, FileWatcherCheckFileTypeChanged), + FileEventTypeNew(REACTOR_SYMLINK_TARGET_CHANGED, FileWatcherCheckSymlinkTargetChanged), + EventTypeNew(REACTOR_FILE_SIZE_EXCEEDS, FILE_SIZE_EXCEEDS_ARGS, + FileWatcherSizeStateNew, FileWatcherStateDestroy, FileWatcherCheckFileSizeExceeds), EventTypeNewNull() }; diff --git a/libpromises/mod_reactor.c b/libpromises/mod_reactor.c index 51610f86f11..139a2825119 100644 --- a/libpromises/mod_reactor.c +++ b/libpromises/mod_reactor.c @@ -35,6 +35,16 @@ static const ConstraintSyntax when_constraints[] = /* Row models */ ConstraintSyntaxNewString(REACTOR_FILE_DELETED, CF_ANYSTRING, "File to react for on deletion", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_CREATED, CF_ANYSTRING, "File to react for on creation", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_MODIFIED, CF_ANYSTRING, "File to react for on change of its modification time", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_CONTENT_MODIFIED, CF_ANYSTRING, "File to react for on change of its content hash", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_PERMISSIONS_CHANGED, CF_ANYSTRING, "File to react for on change of its permissions", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_OWNER_CHANGED, CF_ANYSTRING, "File to react for on change of its owning user or group", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_REPLACED, CF_ANYSTRING, "File to react for on replacement by another file (inode)", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_TYPE_CHANGED, CF_ANYSTRING, "File to react for on change of its type", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_SYMLINK_TARGET_CHANGED, CF_ANYSTRING, "Symlink to react for on change of its target", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewString(REACTOR_FILE_SIZE_EXCEEDS, CF_ANYSTRING, "File to react for when its size grows above '" REACTOR_SIZE_THRESHOLD "'", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewInt(REACTOR_SIZE_THRESHOLD, CF_VALRANGE, "Size in bytes above which '" REACTOR_FILE_SIZE_EXCEEDS "' fires", SYNTAX_STATUS_NORMAL), /* default */ ConstraintSyntaxNewInt("interval", CF_NZERO_VALRANGE, "Seconds between two checks for the event", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewNull() diff --git a/libpromises/mod_reactor.h b/libpromises/mod_reactor.h index b26d3d1f93a..d6f8d82adc2 100644 --- a/libpromises/mod_reactor.h +++ b/libpromises/mod_reactor.h @@ -28,6 +28,16 @@ #include #define REACTOR_FILE_DELETED "file_deleted" +#define REACTOR_FILE_CREATED "file_created" +#define REACTOR_FILE_MODIFIED "file_modified" +#define REACTOR_FILE_CONTENT_MODIFIED "file_content_modified" +#define REACTOR_FILE_PERMISSIONS_CHANGED "file_permissions_changed" +#define REACTOR_FILE_OWNER_CHANGED "file_owner_changed" +#define REACTOR_FILE_REPLACED "file_replaced" +#define REACTOR_FILE_TYPE_CHANGED "file_type_changed" +#define REACTOR_SYMLINK_TARGET_CHANGED "symlink_target_changed" +#define REACTOR_FILE_SIZE_EXCEEDS "file_size_exceeds" +#define REACTOR_SIZE_THRESHOLD "size_threshold" extern const PromiseTypeSyntax CF_REACTOR_PROMISE_TYPES[];