Skip to content
Open
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: 5 additions & 0 deletions cf-agent/files_edit.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
copy equal to the copied template file - not the
copied + edited file. */

/* The ifelapsed of the promises of edit bundles: CF_EDIT_IFELAPSED, unless the
* default ifelapsed is lower, e.g. 0 for the bundles cf-reactor runs on events,
* which may happen less than a minute apart */
#define EDIT_IFELAPSED MIN(CF_EDIT_IFELAPSED, VIFELAPSED)

typedef struct
{
char *filename;
Expand Down
8 changes: 4 additions & 4 deletions cf-agent/files_editline.c
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ static PromiseResult VerifyLineDeletions(EvalContext *ctx, const Promise *pp, Ed
char lockname[CF_BUFSIZE];

Attributes a = GetDeletionAttributes(ctx, pp);
a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

if (!SanityCheckDeletions(&a, pp))
{
Expand Down Expand Up @@ -453,7 +453,7 @@ static PromiseResult VerifyColumnEdits(EvalContext *ctx, const Promise *pp, Edit
char lockname[CF_BUFSIZE];

Attributes a = GetColumnAttributes(ctx, pp);
a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

if (a.column.column_separator == NULL)
{
Expand Down Expand Up @@ -547,7 +547,7 @@ static PromiseResult VerifyPatterns(EvalContext *ctx, const Promise *pp, EditCon
/* Are we working in a restricted region? */

Attributes a = GetReplaceAttributes(ctx, pp);
a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

if (!a.replace.replace_value)
{
Expand Down Expand Up @@ -733,7 +733,7 @@ static PromiseResult VerifyLineInsertions(EvalContext *ctx, const Promise *pp, E

Attributes a = GetInsertionAttributes(ctx, pp);
int allow_multi_lines = StringEqual(a.sourcetype, "preserve_all_lines");
a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

if (!SanityCheckInsertions(&a))
{
Expand Down
16 changes: 8 additions & 8 deletions cf-agent/files_editxml.c
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ static bool VerifyXPathBuild(EvalContext *ctx, const Attributes *attr, const Pro
CfLock thislock;
char lockname[CF_BUFSIZE], rawxpath[CF_BUFSIZE] = { 0 };

a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

int ret = snprintf(rawxpath, sizeof(rawxpath), "%s",
a.xml.havebuildxpath ? a.xml.build_xpath : pp->promiser);
Expand Down Expand Up @@ -417,7 +417,7 @@ static PromiseResult VerifyTreeDeletions(EvalContext *ctx, const Attributes *att
CfLock thislock;
char lockname[CF_BUFSIZE];

a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

if (!SanityCheckTreeDeletions(&a))
{
Expand Down Expand Up @@ -497,7 +497,7 @@ static PromiseResult VerifyTreeInsertions(EvalContext *ctx, const Attributes *at
CfLock thislock;
char lockname[CF_BUFSIZE];

a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

PromiseResult result = PROMISE_RESULT_NOOP;
if (!SanityCheckTreeInsertions(&a, edcontext))
Expand Down Expand Up @@ -590,7 +590,7 @@ static PromiseResult VerifyAttributeDeletions(EvalContext *ctx, const Attributes
CfLock thislock;
char lockname[CF_BUFSIZE];

a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

PromiseResult result = PROMISE_RESULT_NOOP;
if (!SanityCheckAttributeDeletions(&a))
Expand Down Expand Up @@ -673,7 +673,7 @@ static PromiseResult VerifyAttributeSet(EvalContext *ctx, const Attributes *attr
CfLock thislock;
char lockname[CF_BUFSIZE];

a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

PromiseResult result = PROMISE_RESULT_NOOP;
if (!SanityCheckAttributeSet(&a))
Expand Down Expand Up @@ -756,7 +756,7 @@ static PromiseResult VerifyTextDeletions(EvalContext *ctx, const Attributes *att
CfLock thislock;
char lockname[CF_BUFSIZE];

a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

PromiseResult result = PROMISE_RESULT_NOOP;
if (!SanityCheckTextDeletions(&a))
Expand Down Expand Up @@ -839,7 +839,7 @@ static PromiseResult VerifyTextSet(EvalContext *ctx, const Attributes *attr, con
CfLock thislock;
char lockname[CF_BUFSIZE];

a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

PromiseResult result = PROMISE_RESULT_NOOP;
if (!SanityCheckTextSet(&a))
Expand Down Expand Up @@ -922,7 +922,7 @@ static PromiseResult VerifyTextInsertions(EvalContext *ctx, const Attributes *at
CfLock thislock;
char lockname[CF_BUFSIZE];

a.transaction.ifelapsed = CF_EDIT_IFELAPSED;
a.transaction.ifelapsed = EDIT_IFELAPSED;

PromiseResult result = PROMISE_RESULT_NOOP;
if (!SanityCheckTextInsertions(&a))
Expand Down
20 changes: 19 additions & 1 deletion cf-reactor/reactor_transform.c
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,25 @@ static PromiseResult KeepEventsPromiseOnEvent(EvalContext *ctx, const Promise *p
return PROMISE_RESULT_SKIPPED;
}

return RunThenBundles(ctx, pp);
PromiseResult result;
switch (event->check)
{
case WATCHER_CHECK_EVENT:
result = RunThenBundles(ctx, pp);
break;

case WATCHER_CHECK_ERROR:
/* Already logged as an error by the watcher, see HandleQueuedEvents() */
result = PROMISE_RESULT_FAIL;
break;

default:
ProgrammingError("Unexpected watcher check result %d for reactor events promise '%s'",
(int) event->check, pp->promiser);
}

RecordEventsPromiseOutcome(ctx, pp, result);
return result;
}

void HandleReactorEvent(EvalContext *ctx, const Promise *pp, const char *promise_hash, WatcherCheckResult check)
Expand Down
10 changes: 9 additions & 1 deletion cf-reactor/watcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ bool EventWatcherInitialize(int *fd)

*fd = WakeupChannelReadFd(&wakeup_channel);

Log(LOG_LEVEL_VERBOSE, "Started cf-reactor event watcher with %zu watcher(s)", MapSize(watchers));
/* No watchers yet, they are registered from the policy afterwards and
* counted by EventWatcherWakeUp() */
Log(LOG_LEVEL_VERBOSE, "Started cf-reactor event watcher");
return true;
}

Expand Down Expand Up @@ -395,6 +397,12 @@ void EventWatcherResume(void)

void EventWatcherWakeUp(void)
{
/* Called once the watchers of the (re-)read policy are registered */
ThreadLock(&watchers_mutex);
const size_t count = MapSize(watchers);
ThreadUnlock(&watchers_mutex);
Log(LOG_LEVEL_VERBOSE, "cf-reactor event watcher has %zu watcher(s)", count);

if (watcher_thread != NULL)
{
StoppableThreadWake(watcher_thread);
Expand Down
3 changes: 2 additions & 1 deletion cf-reactor/watcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ void EventWatcherResume(void);
* immediately. Without watchers, the watcher thread sleeps until woken up, so
* this must be called after registering watchers outside of an
* EventWatcherPause()/EventWatcherResume() pair (which wakes it up itself),
* i.e. after the watchers of the initial policy.
* i.e. after the watchers of the initial policy. Logs the number of
* registered watchers.
*/
void EventWatcherWakeUp(void);
void EventWatcherFinalize(void);
Expand Down
6 changes: 4 additions & 2 deletions libpromises/cf3parse_logic.h
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,13 @@ static bool RelevantBundle(const char *agent, const char *blocktype)
return true;
}

/* Here are some additional bundle types handled by cfAgent */
/* Here are some additional bundle types handled by cfAgent, and by
* cf-reactor for the agent bundles it runs */

Item *ip = SplitString("edit_line,edit_xml", ',');

if (strcmp(agent, CF_AGENTTYPES[AGENT_TYPE_AGENT]) == 0)
if (StringEqual(agent, CF_AGENTTYPES[AGENT_TYPE_AGENT]) ||
StringEqual(agent, CF_AGENTTYPES[AGENT_TYPE_REACTOR]))
{
if (IsItemIn(ip, blocktype))
{
Expand Down
25 changes: 25 additions & 0 deletions tests/reactor/lib.cf
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,28 @@ bundle agent record_event(file, message)
with => strftime("localtime", "%Y-%m-%d %H:%M:%S", now()),
report_to_file => "$(file)";
}

# Defines a class for the outcome of the promise it is used on:
# <prefix>_kept, <prefix>_repaired or <prefix>_failed
body classes then_outcome(prefix)
{
promise_kept => { "$(prefix)_kept" };
promise_repaired => { "$(prefix)_repaired" };
repair_failed => { "$(prefix)_failed" };
repair_denied => { "$(prefix)_failed" };
repair_timeout => { "$(prefix)_failed" };
}

# Records "<prefix> <outcome>" for the outcome classes of then_outcome(prefix)
# defined so far. Nothing is recorded if the promise was not evaluated (e.g.
# skipped because of a lock).
bundle agent record_then_outcome(file, prefix)
{
vars:
"outcomes" slist => { "kept", "repaired", "failed" };

methods:
"$(prefix)_$(outcomes)"
if => "$(prefix)_$(outcomes)",
usebundle => record_event("$(file)", "$(prefix) $(outcomes)");
}
27 changes: 27 additions & 0 deletions tests/reactor/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ wait_for_log() {
log_offset=$((log_offset + ${match%%:*}))
}

# Wait until cf-reactor has registered the watchers of the policy, $1 of them.
# The watcher thread may log before this line, so search the whole log and
# leave the offset of wait_for_log as is.
wait_for_watchers() {
if ! timeout 60 sh -c '
until grep -q -F -- "$2" "$1"; do
sleep 0.1
done' _ "$LOG" "verbose: cf-reactor event watcher has $1 watcher(s)"
then
echo "Timed out waiting for $1 watcher(s) in reactor log:" >&2
cat "$LOG" >&2
return 1
fi
}

# Wait until file $1 has at least $2 lines, then fail if it has more than $2.
wait_for_lines() {
if ! timeout 60 sh -c '
Expand All @@ -39,6 +54,18 @@ wait_for_lines() {
fi
}

# Fail unless line $2 of file $1, without the timestamp written by
# record_event, is $3.
check_event() {
line=$(sed -n "$2p" "$1" | cut -d' ' -f3-)
if [ "$line" != "$3" ]
then
echo "Expected line $2 of '$1' to be '$3', got '$line':" >&2
cat "$1" >&2
return 1
fi
}

# Start cf-reactor in the background with policy file $1, logging to $LOG.
# It is stopped when the test exits.
start_reactor() {
Expand Down
4 changes: 2 additions & 2 deletions tests/reactor/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
fail=0

banner() {
name=" $(basename "$1") "
name=" $(basename "$(dirname "$1")")/$(basename "$1") "
width=72
left=$(( (width - ${#name}) / 2 ))
right=$(( width - ${#name} - left ))
Expand All @@ -23,7 +23,7 @@ error() {
CF_BINDIR=${CF_BINDIR:-/var/cfengine/bin}
export CF_BINDIR

for t in "$PWD"/tests/*.sh; do
for t in "$PWD"/tests/*/*.sh; do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this change in this commit with "Moved 001_vars test into 001_basics/" ?

@victormlg victormlg Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we want to find all the tests in the tests/ folder. There is tests/00_basics/*.sh , tests/01_event_types/*.sh , 02_then_bundles/*.sh , ...

banner "$t"

# create throwaway work dir
Expand Down
29 changes: 29 additions & 0 deletions tests/reactor/tests/00_basics/001_vars.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#######################################################
#
# Test vars promises in reactor bundle
#
#######################################################
body common control
{
inputs => { "../../lib.cf" };
bundlesequence => { "test" };
version => "1.0";
}

body when deleted(file)
{
file_deleted => "$(file)";
}

bundle reactor test
{
vars:
"test" string => getenv("TESTFILE", 999);
"events_file" string => getenv("EVENTS_FILE", 999);
"promise_name" string => basename("$(this.promise_filename)", ".cf");

events:
"$(promise_name)"
when => deleted("$(test)"),
then => record_event("$(events_file)", "$(test) was deleted");
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

set -ex

. "$(dirname "$0")/../lib.sh"
. "$(dirname "$0")/../../lib.sh"

TESTFILE="$WORKDIR/tmp/test"
EVENTS_FILE="$WORKDIR/tmp/events"
Expand Down
38 changes: 38 additions & 0 deletions tests/reactor/tests/00_basics/002_classes.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#######################################################

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this far and see this PR is quite long. I can continue the review next week.

#
# Test classes promises in reactor bundle
#
#######################################################
body common control
{
inputs => { "../../lib.cf" };
bundlesequence => { "test" };
version => "1.0";
}

body when deleted(file)
{
file_deleted => "$(file)";
}

bundle reactor test
{
vars:
"test" string => getenv("TESTFILE", 999);
"events_file" string => getenv("EVENTS_FILE", 999);
"promise_name" string => basename("$(this.promise_filename)", ".cf");

classes:
"true" expression => "true";

events:
true::
"$(promise_name)"
when => deleted("$(test)"),
then => record_event("$(events_file)", "$(test) was deleted");

false::
"$(promise_name)_2"
when => deleted("$(test)"),
then => record_event("$(events_file)", "$(test) was deleted");
}
26 changes: 26 additions & 0 deletions tests/reactor/tests/00_basics/002_classes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/sh

set -ex

. "$(dirname "$0")/../../lib.sh"

TESTFILE="$WORKDIR/tmp/test"
EVENTS_FILE="$WORKDIR/tmp/events"
export TESTFILE EVENTS_FILE

# create initial file
touch "$TESTFILE"

start_reactor "$1"

wait_for_watchers 1

wait_for_log "File '$TESTFILE' is present, watching for its deletion"
rm "$TESTFILE"
wait_for_lines "$EVENTS_FILE" 1

# recreate the file and wait for the watcher to see it before deleting it again
touch "$TESTFILE"
wait_for_log "File '$TESTFILE' is present, watching for its deletion"
rm "$TESTFILE"
wait_for_lines "$EVENTS_FILE" 2
Loading
Loading