From e7df32676f132f6256562ec53859c411f3df2777 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 2 Aug 2026 11:39:36 +0200 Subject: [PATCH 1/9] package/mdns-alias: upgrade to v1.3 Fixes crash on SIGHUP while disconnected from Avahi, e.g., when the hostname change that triggered the reload also restarted avahi-daemon: mdns-alias[8943]: Failed creating new entry group: Bad state finit[1]: Service mdns-alias keeps crashing, not restarting. Entry group failures, CNAME collisions, and publish errors are now treated as transient and retried with a full reconnect. Also quieter logs by default, routine lifecycle events demoted to INFO. Release notes: https://github.com/troglobit/mdns-alias/releases/tag/v1.3 Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 5 +++++ package/mdns-alias/mdns-alias.hash | 2 +- package/mdns-alias/mdns-alias.mk | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index a800e87c5..df124273d 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -10,6 +10,9 @@ All notable changes to the project are documented in this file. - Upgrade Linux kernel to 6.18.44 (LTS) - Upgrade Buildroot to 2025.02.15 (LTS) +- Upgrade mdns-alias to [v1.3][ma13]: fixes crash on hostname change while + disconnected from Avahi, treats entry group failures and CNAME collisions + as transient (retried instead of exiting), and quieter logs by default - Add support for firewall address-set (ipset): named sets of IP addresses and networks, usable as zone sources for per-IP access control, issue #1189 - Build RPi64 SD card images in release builds @@ -25,6 +28,8 @@ All notable changes to the project are documented in this file. - Fix annoying "cannot deselect all services" or reset to YANG default in the web interface's firewall configuration page +[ma13]: https://github.com/troglobit/mdns-alias/releases/tag/v1.3 + [v26.06.0][] - 2026-07-01 ------------------------- diff --git a/package/mdns-alias/mdns-alias.hash b/package/mdns-alias/mdns-alias.hash index 7263ae672..7e4e9510e 100644 --- a/package/mdns-alias/mdns-alias.hash +++ b/package/mdns-alias/mdns-alias.hash @@ -1,5 +1,5 @@ # From GitHub release -sha256 9f194fa0b6e34fd915054394ef5b820a4f6b1755ace5ed1011bfba6df550accf mdns-alias-1.2.tar.gz +sha256 8186f0758f184cbdcab1033e4945117a587356c323e53bcdd19d47911ee2567b mdns-alias-1.3.tar.gz # Locally generated sha256 3d6f910b5e198f3daab48047b8ee6949040f7abee3927daf2e231f265faf7d91 LICENSE diff --git a/package/mdns-alias/mdns-alias.mk b/package/mdns-alias/mdns-alias.mk index f17147658..5fdeb91b0 100644 --- a/package/mdns-alias/mdns-alias.mk +++ b/package/mdns-alias/mdns-alias.mk @@ -4,7 +4,7 @@ # ################################################################################ -MDNS_ALIAS_VERSION = 1.2 +MDNS_ALIAS_VERSION = 1.3 MDNS_ALIAS_SITE = https://github.com/troglobit/mdns-alias/releases/download/v$(MDNS_ALIAS_VERSION) MDNS_ALIAS_LICENSE = ISC MDNS_ALIAS_LICENSE_FILES = LICENSE From 5d4742a79a2f78fca31bec5c307d4c5c784c21bf Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 2 Aug 2026 10:01:13 +0200 Subject: [PATCH 2/9] statd: batch mDNS neighbor updates, keep main loop responsive Reading operational data can be slow, or time out entirely, while mDNS neighbors are being discovered: statd[3658]: mdns: sr_apply_changes: Timeout expired statd[3658]: Error, getting operational data: User callback failed statd is single threaded; the avahi handlers apply datastore changes per resolver event, from the same event loop that serves all operational get callbacks. Every apply blocks the loop, and resolver events arrive in bursts, e.g., after an mDNS daemon restart. Batch all edits with a debounce timer and apply once the burst has settled. On datastore contention, back off and retry later instead of blocking the loop. Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 6 +++++ src/statd/avahi.c | 66 ++++++++++++++++++++++++++++++++++++++++++----- src/statd/avahi.h | 2 ++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index df124273d..511b78bf0 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -27,6 +27,12 @@ All notable changes to the project are documented in this file. - Fix annoying "cannot deselect all services" or reset to YANG default in the web interface's firewall configuration page +- Fix sporadic slow response, or timeouts, when reading device status while + mDNS neighbors are being discovered, e.g., after an mDNS restart. Updates + to the neighbor table are now batched, and politely retried when other users + or services keep the system busy, logged as: + + statd[3558]: mdns: operational datastore busy, retrying ... [ma13]: https://github.com/troglobit/mdns-alias/releases/tag/v1.3 diff --git a/src/statd/avahi.c b/src/statd/avahi.c index b6619cc5e..4f632f126 100644 --- a/src/statd/avahi.c +++ b/src/statd/avahi.c @@ -354,6 +354,57 @@ static int sr_setstr(sr_session_ctx_t *ses, const char *xpath, const char *val) return err; } +/* + * Resolver events arrive in bursts, e.g., browse storms after an avahi + * restart. Instead of one sr_apply_changes() per event, coalesce all + * edits staged on ctx->sr_ses and apply once the burst settles. On + * datastore contention, back off and retry rather than block -- this + * loop also serves all operational get callbacks. + */ +#define MDNS_APPLY_DEBOUNCE 0.5 +#define MDNS_APPLY_TIMEOUT 1000 /* ms */ +#define MDNS_APPLY_RETRY_MAX 6 /* caps backoff at 0.5 * 2^6 = 32 s */ + +static void ds_apply_cb(struct ev_loop *loop, ev_timer *w, int revents) +{ + struct mdns_ctx *ctx = (struct mdns_ctx *) + ((char *)w - offsetof(struct mdns_ctx, apply_timer)); + int err; + + (void)loop; + (void)revents; + + err = sr_apply_changes(ctx->sr_ses, MDNS_APPLY_TIMEOUT); + switch (err) { + case SR_ERR_OK: + ctx->apply_retries = 0; + break; + case SR_ERR_TIME_OUT: + case SR_ERR_LOCKED: + if (ctx->apply_retries < MDNS_APPLY_RETRY_MAX) + ctx->apply_retries++; + if (ctx->apply_retries == 3) + WARN("mdns: operational datastore busy, retrying ..."); + ev_timer_set(&ctx->apply_timer, MDNS_APPLY_DEBOUNCE * (1 << ctx->apply_retries), 0.0); + ev_timer_start(ctx->loop, &ctx->apply_timer); + break; + default: + ERROR("mdns: sr_apply_changes: %s", sr_strerror(err)); + sr_discard_changes(ctx->sr_ses); + ctx->apply_retries = 0; + break; + } +} + +static void ds_schedule_apply(struct mdns_ctx *ctx) +{ + if (ev_is_active(&ctx->apply_timer)) + return; + + ev_timer_init(&ctx->apply_timer, ds_apply_cb, MDNS_APPLY_DEBOUNCE, 0.0); + ev_timer_start(ctx->loop, &ctx->apply_timer); +} + /* * Return an XPath string literal quoting val: single-quoted unless val * contains a single quote, in which case double quotes are used instead. @@ -437,13 +488,12 @@ static void ds_push_resolver(struct mdns_ctx *ctx, struct avahi_service *svc, } if (err) { + /* drops any coalesced edits too, later events repopulate */ sr_discard_changes(ctx->sr_ses); return; } - err = sr_apply_changes(ctx->sr_ses, 0); - if (err) - ERROR("mdns: sr_apply_changes: %s", sr_strerror(err)); + ds_schedule_apply(ctx); } static void ds_delete_service(struct mdns_ctx *ctx, const char *hostname, const char *name) @@ -470,7 +520,7 @@ static void ds_delete_neighbor(struct mdns_ctx *ctx, const char *hostname) static void ds_clear_all(struct mdns_ctx *ctx) { sr_delete_item(ctx->sr_ses, XPATH_BASE, 0); - sr_apply_changes(ctx->sr_ses, 0); + ds_schedule_apply(ctx); } /* -------------------------------------------------------------------------- @@ -641,7 +691,7 @@ static void service_browser_cb(AvahiServiceBrowser *b, } } - sr_apply_changes(ctx->sr_ses, 0); + ds_schedule_apply(ctx); break; } @@ -973,6 +1023,8 @@ void mdns_ctx_exit(struct mdns_ctx *ctx) ev_timer_stop(ctx->loop, &ctx->reconn_timer); if (ev_is_active(&ctx->retry_timer)) ev_timer_stop(ctx->loop, &ctx->retry_timer); + if (ev_is_active(&ctx->apply_timer)) + ev_timer_stop(ctx->loop, &ctx->apply_timer); /* Free browsers explicitly before freeing the client */ while (!LIST_EMPTY(&ctx->type_entries)) { @@ -991,7 +1043,9 @@ void mdns_ctx_exit(struct mdns_ctx *ctx) } if (ctx->sr_ses) { - ds_clear_all(ctx); + /* event loop is going away, flush synchronously */ + sr_delete_item(ctx->sr_ses, XPATH_BASE, 0); + sr_apply_changes(ctx->sr_ses, MDNS_APPLY_TIMEOUT); sr_session_stop(ctx->sr_ses); ctx->sr_ses = NULL; } diff --git a/src/statd/avahi.h b/src/statd/avahi.h index 88f06a288..598bef793 100644 --- a/src/statd/avahi.h +++ b/src/statd/avahi.h @@ -61,6 +61,8 @@ struct mdns_ctx { unsigned int fail_count; /* Non-zero while avahi-daemon is absent */ ev_timer reconn_timer; /* Free+recreate client after brief delay */ ev_timer retry_timer; /* Deferred warn-log timer */ + ev_timer apply_timer; /* Debounced DS apply, with retry */ + unsigned int apply_retries; LIST_HEAD(, avahi_neighbor) neighbors; LIST_HEAD(, avahi_service) services; /* Flat list; keyed by 5-tuple */ LIST_HEAD(, avahi_type_entry) type_entries; From d61949662ca26c8dd641834e676faadbfccc5e02 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 2 Aug 2026 11:11:42 +0200 Subject: [PATCH 3/9] statd: demote mdns reconnect log messages to INFO Every configuration change that restarts the mDNS daemon logs a NOTICE level reconnect message. Routine noise, demote to INFO; the WARN/NOTE pair for an unresponsive daemon is kept as is. Also, minor code refactor included, dropping code useless block. Signed-off-by: Joachim Wiberg --- src/statd/avahi.c | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/statd/avahi.c b/src/statd/avahi.c index 4f632f126..1063dcee8 100644 --- a/src/statd/avahi.c +++ b/src/statd/avahi.c @@ -384,7 +384,7 @@ static void ds_apply_cb(struct ev_loop *loop, ev_timer *w, int revents) if (ctx->apply_retries < MDNS_APPLY_RETRY_MAX) ctx->apply_retries++; if (ctx->apply_retries == 3) - WARN("mdns: operational datastore busy, retrying ..."); + NOTE("mdns: operational datastore busy, retrying ..."); ev_timer_set(&ctx->apply_timer, MDNS_APPLY_DEBOUNCE * (1 << ctx->apply_retries), 0.0); ev_timer_start(ctx->loop, &ctx->apply_timer); break; @@ -838,6 +838,7 @@ static void reconn_cb(struct ev_loop *loop, ev_timer *w, int revents) * that a normal daemon restart cancels this timer before it fires. */ #define MDNS_WARN_DELAY 10.0 +#define MDNS_FAIL_ESCALATE 3 /* NOTE level after 3 x MDNS_WARN_DELAY */ static void mdns_retry_cb(struct ev_loop *loop, ev_timer *w, int revents) { @@ -848,8 +849,12 @@ static void mdns_retry_cb(struct ev_loop *loop, ev_timer *w, int revents) (void)revents; ctx->fail_count++; - if (mdns_is_enabled(ctx)) - WARN("mdns: mDNS daemon not responding, will reconnect automatically"); + if (mdns_is_enabled(ctx)) { + if (ctx->fail_count >= MDNS_FAIL_ESCALATE) + NOTE("mdns: mDNS daemon still not responding, will keep trying"); + else + INFO("mdns: mDNS daemon not responding, will reconnect automatically"); + } } static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata) @@ -863,7 +868,10 @@ static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata) if (ctx->fail_count > 0) { ev_timer_stop(ctx->loop, &ctx->reconn_timer); ev_timer_stop(ctx->loop, &ctx->retry_timer); - NOTE("mdns: mDNS daemon reconnected"); + if (ctx->fail_count >= MDNS_FAIL_ESCALATE) + NOTE("mdns: mDNS daemon reconnected"); + else + INFO("mdns: mDNS daemon reconnected"); ctx->fail_count = 0; } INFO("mdns: client running"); @@ -900,16 +908,15 @@ static void client_cb(AvahiClient *c, AvahiClientState state, void *userdata) ev_timer_start(ctx->loop, &ctx->retry_timer); } - { + while (!LIST_EMPTY(&ctx->type_entries)) { struct avahi_type_entry *te; - while (!LIST_EMPTY(&ctx->type_entries)) { - te = LIST_FIRST(&ctx->type_entries); - avahi_service_browser_free(te->browser); - LIST_REMOVE(te, link); - free(te); - } + te = LIST_FIRST(&ctx->type_entries); + avahi_service_browser_free(te->browser); + LIST_REMOVE(te, link); + free(te); } + if (ctx->type_browser) { avahi_service_type_browser_free(ctx->type_browser); ctx->type_browser = NULL; @@ -979,11 +986,11 @@ void mdns_ctx_reconnect(struct mdns_ctx *ctx) int avahi_err; if (!mdns_is_enabled(ctx)) { - NOTE("mdns: mDNS is disabled, ignoring reconnect request"); + INFO("mdns: mDNS is disabled, ignoring reconnect request"); return; } - NOTE("mdns: reconnecting on request"); + INFO("mdns: reconnecting on request"); ev_timer_stop(ctx->loop, &ctx->reconn_timer); ev_timer_stop(ctx->loop, &ctx->retry_timer); From d7f017d6a6b3ffa27047031feea7883fff7b0dce Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 2 Aug 2026 11:11:41 +0200 Subject: [PATCH 4/9] statd: add usage, verbosity option, and setlogmask() support statd logs everything, including INFO level messages, to syslog by default, unlike other services which default to NOTICE. Port option handling from confd: -h, -V, and -v , backed by setlogmask(). Drop the stale flags from statd.conf, they were never parsed and are rejected now that statd has strict option handling. Unlike confd, -v info maps to LOG_INFO rather than LOG_NOTICE, and -v debug also enables the DEBUG() macro without requiring the DEBUG environment variable. Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 2 + package/statd/statd.conf | 2 +- src/statd/Makefile.am | 1 + src/statd/statd.c | 73 +++++++++++++++++++- test/case/statd/system/system/run/initctl_-j | 2 +- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 511b78bf0..9b0914db0 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -17,6 +17,8 @@ All notable changes to the project are documented in this file. networks, usable as zone sources for per-IP access control, issue #1189 - Build RPi64 SD card images in release builds - Include .pkg files in release builds +- The `statd` service now logs at `notice` level by default, like other + services, and supports `-v ` to adjust verbosity at runtime ### Added diff --git a/package/statd/statd.conf b/package/statd/statd.conf index 5d76de659..cbc1e9d9c 100644 --- a/package/statd/statd.conf +++ b/package/statd/statd.conf @@ -1,2 +1,2 @@ #set DEBUG=1 -service name:statd [12345] statd -f -p /run/statd.pid -n -- Status daemon +service name:statd [12345] statd -- Status daemon diff --git a/src/statd/Makefile.am b/src/statd/Makefile.am index 727583daa..6b4488522 100644 --- a/src/statd/Makefile.am +++ b/src/statd/Makefile.am @@ -4,6 +4,7 @@ ACLOCAL_AMFLAGS = -I m4 sbin_PROGRAMS = statd statd_SOURCES = statd.c shared.c shared.h journal.c journal_retention.c journal.h avahi.c avahi.h statd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE +statd_CPPFLAGS += -DSTATD_VERSION=\"$(PACKAGE_VERSION)\" statd_CFLAGS = -W -Wall -Wextra statd_CFLAGS += $(jansson_CFLAGS) $(libyang_CFLAGS) $(sysrepo_CFLAGS) statd_CFLAGS += $(libsrx_CFLAGS) $(libite_CFLAGS) diff --git a/src/statd/statd.c b/src/statd/statd.c index dac055836..cb2a18ee3 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -1,9 +1,11 @@ /* SPDX-License-Identifier: BSD-3-Clause */ +#include #include #include #include #include +#include #include #include #include @@ -463,21 +465,86 @@ static int subscribe_to_all(struct statd *statd) return SR_ERR_OK; } +static void version_print(void) +{ + printf("statd - status daemon v%s, compiled with libsysrepo v%s\n\n", + STATD_VERSION, SR_VERSION); +} + +static void help_print(void) +{ + printf("Usage:\n" + " statd [-h] [-V] [-v ]\n" + "\n" + "Options:\n" + " -h, --help Prints usage help.\n" + " -V, --version Prints version information.\n" + " -v, --verbosity \n" + " Change verbosity to a level (none, error, warning, info, debug).\n" + "\n"); +} + int main(int argc, char *argv[]) { struct ev_signal sigint_watcher, sigusr1_watcher, sighup_watcher; int log_opts = LOG_PID | LOG_NDELAY; + int log_level = LOG_NOTICE; struct statd statd = {}; - const char *env; + int opt; int err; - env = getenv("DEBUG"); - if (env || (argc > 1 && !strcmp(argv[1], "-d"))) { + struct option options[] = { + {"help", no_argument, NULL, 'h'}, + {"version", no_argument, NULL, 'V'}, + {"verbosity", required_argument, NULL, 'v'}, + {NULL, 0, NULL, 0}, + }; + + opterr = 0; + while ((opt = getopt_long(argc, argv, "hVv:", options, NULL)) != -1) { + switch (opt) { + case 'h': + version_print(); + help_print(); + return EXIT_SUCCESS; + case 'V': + version_print(); + return EXIT_SUCCESS; + case 'v': + if (!strcmp(optarg, "none")) + log_level = LOG_EMERG; + else if (!strcmp(optarg, "error")) + log_level = LOG_ERR; + else if (!strcmp(optarg, "warning")) + log_level = LOG_WARNING; + else if (!strcmp(optarg, "info")) + log_level = LOG_INFO; + else if (!strcmp(optarg, "debug")) { + log_level = LOG_DEBUG; + debug = 1; + } else { + fprintf(stderr, "statd error: Invalid verbosity \"%s\"\n", optarg); + return EXIT_FAILURE; + } + break; + default: + fprintf(stderr, "statd error: Invalid option or missing argument: -%c\n", optopt); + return EXIT_FAILURE; + } + } + + if (optind < argc) { + fprintf(stderr, "statd error: Redundant parameters\n"); + return EXIT_FAILURE; + } + + if (getenv("DEBUG")) { log_opts |= LOG_PERROR; debug = 1; } openlog("statd", log_opts, LOG_DAEMON); + setlogmask(LOG_UPTO(log_level)); TAILQ_INIT(&statd.subs); statd.ev_loop = EV_DEFAULT; diff --git a/test/case/statd/system/system/run/initctl_-j b/test/case/statd/system/system/run/initctl_-j index 4e9bf0100..f995b21bb 100644 --- a/test/case/statd/system/system/run/initctl_-j +++ b/test/case/statd/system/system/run/initctl_-j @@ -214,7 +214,7 @@ "forking": false, "status": "running", "origin": "/etc/finit.d/enabled/statd.conf", - "command": "statd -f -p /run/statd.pid -n", + "command": "statd", "condition": [ "+pid/confd" ], "restarts": 0, "pidfile": "/run/statd.pid", From 071012c48d9da1a38c9451742402e35e511822e4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 2 Aug 2026 22:36:21 +0200 Subject: [PATCH 5/9] statd: contain journal snapshots in a low-priority child process Configuring the system, or querying status, can be slow or time out entirely while the periodic (5 min) journal snapshot is running. On slow systems with a big configuration a snapshot can take minutes. The snapshot ran in a statd thread, reading ALL operational data in a single sr_get_data("/*") call: every subsystem's callbacks are invoked back-to-back while datastore locks are held for the entire read, with statd's main loop busy serving them. Other datastore users queue up behind it. Fork the snapshot instead, renamed statd-journal using prctl(), running at nice 10 with its own sysrepo connection. The dump is chunked, one module per sr_get_data() call with a short breather in between, so interactive users interleave with the snapshot instead of waiting for all of it. The snapshot timer is one-shot, re-armed only when the previous snapshot has finished: snapshots can never overlap, and the interval is the rest between them rather than a fixed cadence. Each module read waits at most ten seconds: reading operational data holds the module's read lock, also while waiting for providers, so on a busy system the journal skips that module for the round instead of holding up configuration changes: Failed sending edit-config RPC: Locking a rwlock failed (sr_shmmod_lock: Connection timed out), read lock held by running process 28334 (CID 85), running process 3627 (CID 3). Retrying ... Skipped modules are counted in the snapshot completion log message. Also drops the last thread from statd, and the second sysrepo session, which was only used by the journal. Signed-off-by: Joachim Wiberg --- src/statd/journal.c | 277 ++++++++++++++++++++++++++++---------------- src/statd/journal.h | 29 ++--- src/statd/statd.c | 21 +--- 3 files changed, 192 insertions(+), 135 deletions(-) diff --git a/src/statd/journal.c b/src/statd/journal.c index b515c1f1e..6a762a6b6 100644 --- a/src/statd/journal.c +++ b/src/statd/journal.c @@ -1,31 +1,46 @@ /* SPDX-License-Identifier: BSD-3-Clause */ +/* + * Periodic snapshots of the operational datastore for post-mortem and + * trend analysis: /var/lib/statd/operational.json is always the latest, + * with gzipped timestamped archives kept according to the retention + * policy in journal_retention.c. + * + * The work runs in a forked child, renamed statd-journal, at reduced + * priority. This keeps statd's event loop free to serve operational + * get callbacks -- including those triggered by the snapshot itself. + * The dump is chunked per YANG module, releasing all datastore locks + * between each read, so configuration changes and status queries from + * interactive users interleave with the dump instead of queueing up + * behind one long read. + */ + +#include +#include #include #include -#include -#include -#include #include -#include -#include +#include #include -#include -#include +#include +#include +#include #include +#include +#include +#include + #include #include "journal.h" -#define JOURNAL_DIR "/var/lib/statd" -#define DUMP_FILE "/var/lib/statd/operational.json" -#define DUMP_INTERVAL 300.0 /* 5 minutes in seconds */ - -static void journal_stop_cb(struct ev_loop *loop, struct ev_async *, int) -{ - DEBUG("Journal thread stop signal received"); - ev_break(loop, EVBREAK_ALL); -} +#define JOURNAL_DIR "/var/lib/statd" +#define DUMP_FILE JOURNAL_DIR "/operational.json" +#define DUMP_INTERVAL 300.0 /* seconds of rest between snapshots */ +#define CHUNK_DELAY 50000 /* us breather between module reads */ +#define CHUNK_TIMEOUT 10000 /* ms, keep short: the read holds module locks + * that configuration changes wait on */ static void get_timestamp_filename(char *buf, size_t len, time_t ts) { @@ -102,131 +117,187 @@ static int create_snapshot(const struct lyd_node *tree) return 0; } -static void journal_timer_cb(struct ev_loop *, struct ev_timer *w, int) +/* + * Read operational data one module at a time, merging into a single + * tree. Every sr_get_data() releases its locks on return, giving + * other datastore users a chance to run between chunks. + */ +static struct lyd_node *dump_modules(sr_session_ctx_t *ses, const struct ly_ctx *ctx, + int *skipped) +{ + const struct lys_module *mod; + struct lyd_node *tree = NULL; + uint32_t idx = 0; + + while ((mod = ly_ctx_get_module_iter(ctx, &idx))) { + char xpath[300]; + sr_data_t *data; + int err; + + if (!mod->implemented || !mod->compiled || !mod->compiled->data) + continue; + + snprintf(xpath, sizeof(xpath), "/%s:*", mod->name); + err = sr_get_data(ses, xpath, 0, CHUNK_TIMEOUT, 0, &data); + if (err) { + INFO("Skipping %s: %s", mod->name, sr_strerror(err)); + (*skipped)++; + continue; + } + + if (data) { + if (data->tree && lyd_merge_siblings(&tree, data->tree, 0)) + ERROR("Error, merging %s data", mod->name); + sr_release_data(data); + } + + usleep(CHUNK_DELAY); + } + + return tree; +} + +/* + * Forked child: fresh sysrepo connection, dump, archive, retention, + * then _exit() -- never touch inherited statd state. + */ +static void snapshot_process(void) { - struct journal_ctx *jctx = (struct journal_ctx *)w->data; - struct timespec start, end; struct snapshot *snapshots = NULL; - sr_conn_ctx_t *con; + sr_session_ctx_t *ses = NULL; + sr_conn_ctx_t *conn = NULL; + struct timespec start, end; const struct ly_ctx *ctx; - sr_data_t *sr_data = NULL; - sr_error_t err; - int snapshot_count = 0; - long duration_ms; - + struct lyd_node *tree; + int rc = EXIT_FAILURE; + int skipped = 0; + int count = 0; + long ms; + + prctl(PR_SET_NAME, "statd-journal", 0, 0, 0); + closelog(); /* drop log connection inherited from statd */ + openlog("statd-journal", LOG_PID | LOG_NDELAY | (debug ? LOG_PERROR : 0), LOG_DAEMON); + nice(10); + + NOTE("Starting operational datastore snapshot"); clock_gettime(CLOCK_MONOTONIC, &start); - DEBUG("Starting operational datastore dump"); - con = sr_session_get_connection(jctx->sr_query_ses); - if (!con) { - ERROR("Error, getting sr connection for dump"); - return; + if (mkdir(JOURNAL_DIR, 0755) && errno != EEXIST) + ERROR("Error, creating directory " JOURNAL_DIR ": %s", strerror(errno)); + + if (sr_connect(SR_CONN_DEFAULT, &conn)) { + ERROR("Error, connecting to sysrepo"); + _exit(rc); + } + if (sr_session_start(conn, SR_DS_OPERATIONAL, &ses)) { + ERROR("Error, starting session"); + goto done; } - ctx = sr_acquire_context(con); + ctx = sr_acquire_context(conn); if (!ctx) { - ERROR("Error, acquiring context for dump"); - return; + ERROR("Error, acquiring context"); + goto done; } - /* Query ALL operational data via second session - * This triggers our own operational callbacks running in main thread - */ - DEBUG("Calling sr_get_data on session %p", jctx->sr_query_ses); - err = sr_get_data(jctx->sr_query_ses, "/*", 0, 0, 0, &sr_data); - if (err != SR_ERR_OK) { - ERROR("Error, getting operational data: %s", sr_strerror(err)); - sr_release_context(con); - return; - } - DEBUG("sr_get_data succeeded, got data tree: %p", sr_data ? sr_data->tree : NULL); - - /* Create timestamped snapshot */ - if (sr_data && sr_data->tree) { - if (create_snapshot(sr_data->tree) != 0) { - sr_release_data(sr_data); - sr_release_context(con); - return; - } + tree = dump_modules(ses, ctx, &skipped); + if (tree) { + rc = create_snapshot(tree) ? EXIT_FAILURE : EXIT_SUCCESS; + lyd_free_all(tree); } else { DEBUG("No operational data to dump"); + rc = EXIT_SUCCESS; } + sr_release_context(conn); - sr_release_data(sr_data); - sr_release_context(con); - - /* Apply retention policy */ - if (journal_scan_snapshots(JOURNAL_DIR, &snapshots, &snapshot_count) == 0) { - DEBUG("Applying retention policy to %d snapshots", snapshot_count); - journal_apply_retention_policy(JOURNAL_DIR, snapshots, snapshot_count, time(NULL)); + if (journal_scan_snapshots(JOURNAL_DIR, &snapshots, &count) == 0) { + DEBUG("Applying retention policy to %d snapshots", count); + journal_apply_retention_policy(JOURNAL_DIR, snapshots, count, time(NULL)); free(snapshots); } clock_gettime(CLOCK_MONOTONIC, &end); - duration_ms = (end.tv_sec - start.tv_sec) * 1000 + - (end.tv_nsec - start.tv_nsec) / 1000000; + ms = (end.tv_sec - start.tv_sec) * 1000 + + (end.tv_nsec - start.tv_nsec) / 1000000; + if (skipped) + NOTE("Snapshot created and retention applied (took %ld ms, %d modules busy, skipped)", + ms, skipped); + else + NOTE("Snapshot created and retention applied (took %ld ms)", ms); +done: + if (ses) + sr_session_stop(ses); + sr_disconnect(conn); + _exit(rc); +} - INFO("Journal snapshot created and retention applied (took %ld ms)", duration_ms); +/* + * The timer is one-shot, re-armed only when the previous snapshot has + * finished. Snapshots can thus never overlap, and DUMP_INTERVAL is + * the rest between them rather than a fixed cadence -- on a slow, or + * busy, system snapshots are simply taken further apart. + */ +static void journal_rearm(struct journal_ctx *jctx) +{ + ev_timer_set(&jctx->timer, DUMP_INTERVAL, 0.0); + ev_timer_start(jctx->loop, &jctx->timer); } -static void *journal_thread_fn(void *arg) +static void journal_child_cb(struct ev_loop *loop, struct ev_child *w, int revents) { - struct journal_ctx *jctx = (struct journal_ctx *)arg; - struct ev_timer journal_timer; + struct journal_ctx *jctx = (struct journal_ctx *) + ((char *)w - offsetof(struct journal_ctx, child)); - INFO("Journal thread started"); + (void)revents; - if (mkdir("/var/lib/statd", 0755) != 0 && errno != EEXIST) { - ERROR("Error, creating directory /var/lib/statd: %s", strerror(errno)); - } + ev_child_stop(loop, w); + jctx->pid = 0; - jctx->journal_loop = ev_loop_new(EVFLAG_AUTO); - if (!jctx->journal_loop) { - ERROR("Error, creating journal thread event loop"); - return NULL; - } + if (!WIFEXITED(w->rstatus) || WEXITSTATUS(w->rstatus)) + ERROR("Journal snapshot failed, status %d", w->rstatus); - /* Setup async watcher for stop signal */ - ev_async_init(&jctx->journal_stop, journal_stop_cb); - ev_async_start(jctx->journal_loop, &jctx->journal_stop); + journal_rearm(jctx); +} - /* Setup timer for periodic dumps */ - ev_timer_init(&journal_timer, journal_timer_cb, DUMP_INTERVAL, DUMP_INTERVAL); - journal_timer.data = jctx; - ev_timer_start(jctx->journal_loop, &journal_timer); +static void journal_timer_cb(struct ev_loop *loop, ev_timer *w, int revents) +{ + struct journal_ctx *jctx = (struct journal_ctx *) + ((char *)w - offsetof(struct journal_ctx, timer)); + pid_t pid; - DEBUG("Journal thread entering event loop"); - ev_run(jctx->journal_loop, 0); + (void)revents; - ev_timer_stop(jctx->journal_loop, &journal_timer); - ev_async_stop(jctx->journal_loop, &jctx->journal_stop); - ev_loop_destroy(jctx->journal_loop); + pid = fork(); + if (pid < 0) { + ERRNO("Failed forking journal snapshot process"); + journal_rearm(jctx); + return; + } + if (!pid) + snapshot_process(); /* never returns */ - INFO("Journal thread exiting"); - return NULL; + jctx->pid = pid; + ev_child_init(&jctx->child, journal_child_cb, pid, 0); + ev_child_start(loop, &jctx->child); } -int journal_start(struct journal_ctx *jctx, sr_session_ctx_t *sr_query_ses) +int journal_start(struct journal_ctx *jctx, struct ev_loop *loop) { - int err; + jctx->loop = loop; + jctx->pid = 0; - jctx->sr_query_ses = sr_query_ses; - jctx->journal_thread_running = 1; + ev_timer_init(&jctx->timer, journal_timer_cb, DUMP_INTERVAL, 0.0); + ev_timer_start(loop, &jctx->timer); - err = pthread_create(&jctx->journal_thread, NULL, journal_thread_fn, jctx); - if (err) { - ERROR("Error, creating journal thread: %s", strerror(err)); - return err; - } - - INFO("Periodic operational dump enabled (every %.0f seconds)", DUMP_INTERVAL); + NOTE("Periodic operational snapshot enabled (every %.0f seconds)", DUMP_INTERVAL); return 0; } void journal_stop(struct journal_ctx *jctx) { - /* Signal thread to exit immediately via async watcher */ - jctx->journal_thread_running = 0; - ev_async_send(jctx->journal_loop, &jctx->journal_stop); - pthread_join(jctx->journal_thread, NULL); + ev_timer_stop(jctx->loop, &jctx->timer); + + /* Snapshot in progress completes on its own, reaped by init */ + if (jctx->pid) + ev_child_stop(jctx->loop, &jctx->child); } diff --git a/src/statd/journal.h b/src/statd/journal.h index dc5784fa7..7080854f5 100644 --- a/src/statd/journal.h +++ b/src/statd/journal.h @@ -3,27 +3,28 @@ #ifndef STATD_JOURNAL_H_ #define STATD_JOURNAL_H_ -#include -#include -#include #include -/* Snapshot structure for tracking journal files */ -struct snapshot { - char filename[256]; - time_t timestamp; -}; +#ifndef JOURNAL_RETENTION_STUB +#include +#include struct journal_ctx { - sr_session_ctx_t *sr_query_ses; /* Consumer session for queries */ - struct ev_loop *journal_loop; /* Event loop for journal thread */ - pthread_t journal_thread; /* Thread for periodic dumps */ - struct ev_async journal_stop; /* Signal to stop journal thread */ - volatile int journal_thread_running; /* Flag to stop journal thread */ + struct ev_loop *loop; + ev_timer timer; /* Periodic snapshot trigger */ + struct ev_child child; /* Reaper for the snapshot process */ + pid_t pid; /* Non-zero while a snapshot is running */ }; -int journal_start(struct journal_ctx *jctx, sr_session_ctx_t *sr_query_ses); +int journal_start(struct journal_ctx *jctx, struct ev_loop *loop); void journal_stop(struct journal_ctx *jctx); +#endif + +/* Snapshot structure for tracking journal files */ +struct snapshot { + char filename[256]; + time_t timestamp; +}; int journal_scan_snapshots(const char *dir, struct snapshot **snapshots, int *count); void journal_apply_retention_policy(const char *dir, struct snapshot *snapshots, int count, time_t now); diff --git a/src/statd/statd.c b/src/statd/statd.c index cb2a18ee3..997055ba9 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -11,7 +11,6 @@ #include #include #include -#include #include #include @@ -69,10 +68,9 @@ struct sub { struct statd { struct sub_head subs; sr_session_ctx_t *sr_ses; /* Provider session with callbacks */ - sr_session_ctx_t *sr_query_ses; /* Consumer session for queries */ sr_conn_ctx_t *sr_conn; /* Connection (owns YANG context) */ struct ev_loop *ev_loop; - struct journal_ctx journal; /* Journal thread context */ + struct journal_ctx journal; /* Periodic operational snapshots */ struct mdns_ctx mdns; /* mDNS neighbor monitor */ }; @@ -558,7 +556,7 @@ int main(int argc, char *argv[]) } DEBUG("Connected to sysrepo"); - /* Session 1: Provider with operational callbacks */ + /* Provider session with operational callbacks */ err = sr_session_start(statd.sr_conn, SR_DS_OPERATIONAL, &statd.sr_ses); if (err) { ERROR("Error, start provider session: %s", sr_strerror(err)); @@ -567,19 +565,8 @@ int main(int argc, char *argv[]) } DEBUG("Provider session started (%p)", statd.sr_ses); - /* Session 2: Consumer for querying operational data */ - err = sr_session_start(statd.sr_conn, SR_DS_OPERATIONAL, &statd.sr_query_ses); - if (err) { - ERROR("Error, start query session: %s", sr_strerror(err)); - sr_session_stop(statd.sr_ses); - sr_disconnect(statd.sr_conn); - return EXIT_FAILURE; - } - DEBUG("Query session started (%p)", statd.sr_query_ses); - err = subscribe_to_all(&statd); if (err) { - sr_session_stop(statd.sr_query_ses); sr_session_stop(statd.sr_ses); sr_disconnect(statd.sr_conn); return EXIT_FAILURE; @@ -597,9 +584,8 @@ int main(int argc, char *argv[]) sighup_watcher.data = &statd; ev_signal_start(statd.ev_loop, &sighup_watcher); - err = journal_start(&statd.journal, statd.sr_query_ses); + err = journal_start(&statd.journal, statd.ev_loop); if (err) { - sr_session_stop(statd.sr_query_ses); sr_session_stop(statd.sr_ses); sr_disconnect(statd.sr_conn); return EXIT_FAILURE; @@ -621,7 +607,6 @@ int main(int argc, char *argv[]) journal_stop(&statd.journal); unsub_to_all(&statd); - sr_session_stop(statd.sr_query_ses); sr_session_stop(statd.sr_ses); sr_disconnect(statd.sr_conn); From 5bb61a9156b46733f9723182c046744016a6c51e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 2 Aug 2026 23:23:12 +0200 Subject: [PATCH 6/9] statd: identify module and exit code in yanger error messages A failing yanger invocation logged only: statd[3539]: Error, running yanger statd[3539]: Error adding interface yanger data leaving no trace of which model or interface failed, or how. Include the model, the interface for per-interface queries, and yanger's exit code. Also plug a small memory leak: the extracted interface name was never freed after the query. Signed-off-by: Joachim Wiberg --- src/statd/statd.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/statd/statd.c b/src/statd/statd.c index 997055ba9..93dd9ad44 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -97,7 +97,8 @@ static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent err = fsystemv(yanger_args, NULL, stream, NULL); if (err) { - ERROR("Error, running yanger"); + ERROR("Error, running yanger %s%s%s, exit code %d", yanger_args[1], + yanger_args[3] ? " " : "", yanger_args[3] ?: "", err); fclose(stream); return SR_ERR_SYS; } @@ -191,8 +192,9 @@ static int sr_iface_cb(sr_session_ctx_t *session, uint32_t, const char *model, } err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding interface yanger data"); + ERROR("Error adding yanger data for %s", ifname ?: model); + free(ifname); sr_release_context(con); return SR_ERR_OK; @@ -227,7 +229,7 @@ static int sr_generic_cb(sr_session_ctx_t *session, uint32_t, const char *model, err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data"); + ERROR("Error adding yanger data for %s", yanger_args[1]); sr_release_context(con); @@ -263,7 +265,7 @@ static int sr_ospf_cb(sr_session_ctx_t *session, uint32_t, const char *, err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data"); + ERROR("Error adding yanger data for %s", yanger_args[1]); sr_release_context(con); @@ -299,7 +301,7 @@ static int sr_rip_cb(sr_session_ctx_t *session, uint32_t, const char *, err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data"); + ERROR("Error adding yanger data for %s", yanger_args[1]); sr_release_context(con); @@ -335,7 +337,7 @@ static int sr_bfd_cb(sr_session_ctx_t *session, uint32_t, const char *, err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data"); + ERROR("Error adding yanger data for %s", yanger_args[1]); sr_release_context(con); From c0b3233d4e28a474a04434ca60b0e53266c99f35 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 3 Aug 2026 10:47:13 +0200 Subject: [PATCH 7/9] statd: audit error messages' grammar Signed-off-by: Joachim Wiberg --- src/statd/statd.c | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/statd/statd.c b/src/statd/statd.c index 93dd9ad44..e338725f0 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -97,7 +97,7 @@ static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent err = fsystemv(yanger_args, NULL, stream, NULL); if (err) { - ERROR("Error, running yanger %s%s%s, exit code %d", yanger_args[1], + ERROR("Error calling yanger %s%s%s, exit code %d", yanger_args[1], yanger_args[3] ? " " : "", yanger_args[3] ?: "", err); fclose(stream); return SR_ERR_SYS; @@ -113,7 +113,7 @@ static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent err = lyd_parse_data_fd(ctx, fd, LYD_JSON, LYD_PARSE_ONLY, 0, parent); if (err) - ERROR("Error, parsing yanger data (%d): %s", err, ly_errmsg(ctx)); + ERROR("Failed parsing yanger data (%d): %s", err, ly_errmsg(ctx)); fclose(stream); /* Note: fclose() already closes the underlying fd from fdopen() */ @@ -136,12 +136,12 @@ static char *xpath_extract(const char *xpath, const char *key) end = strchr(ptr, '\''); if (!end) { - ERROR("Can't find end quote for %s (sanity check)", key); + ERROR("Cannot find end quote for %s (sanity check)", key); return NULL; } if ((end - ptr) >= XPATH_MAX) { - ERROR("Value for %s is to long (sanity check)", key); + ERROR("Value for %s is too long (sanity check)", key); return NULL; } @@ -175,13 +175,13 @@ static int sr_iface_cb(sr_session_ctx_t *session, uint32_t, const char *model, con = sr_session_get_connection(session); if (!con) { - ERROR("Error, getting sr connection"); + ERROR("Error getting sysrepo connection"); return SR_ERR_INTERNAL; } ctx = sr_acquire_context(con); if (!ctx) { - ERROR("Error, acquiring context"); + ERROR("Failed acquiring sysrepo context"); return SR_ERR_INTERNAL; } @@ -192,7 +192,7 @@ static int sr_iface_cb(sr_session_ctx_t *session, uint32_t, const char *model, } err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data for %s", ifname ?: model); + ERROR("Failed adding yanger data for %s", ifname ?: model); free(ifname); sr_release_context(con); @@ -217,19 +217,19 @@ static int sr_generic_cb(sr_session_ctx_t *session, uint32_t, const char *model, con = sr_session_get_connection(session); if (!con) { - ERROR("Error, getting sr connection"); + ERROR("Error getting sysrepo connection"); return SR_ERR_INTERNAL; } ctx = sr_acquire_context(con); if (!ctx) { - ERROR("Error, acquiring context"); + ERROR("Failed acquiring sysrepo context"); return SR_ERR_INTERNAL; } err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data for %s", yanger_args[1]); + ERROR("Failed adding yanger data for %s", yanger_args[1]); sr_release_context(con); @@ -253,19 +253,19 @@ static int sr_ospf_cb(sr_session_ctx_t *session, uint32_t, const char *, con = sr_session_get_connection(session); if (!con) { - ERROR("Error, getting sr connection"); + ERROR("Error getting sysrepo connection"); return SR_ERR_INTERNAL; } ctx = sr_acquire_context(con); if (!ctx) { - ERROR("Error, acquiring context"); + ERROR("Failed acquiring sysrepo context"); return SR_ERR_INTERNAL; } err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data for %s", yanger_args[1]); + ERROR("Failed adding yanger data for %s", yanger_args[1]); sr_release_context(con); @@ -285,23 +285,23 @@ static int sr_rip_cb(sr_session_ctx_t *session, uint32_t, const char *, sr_conn_ctx_t *con; sr_error_t err; - DEBUG("Incoming rip query for xpath: %s", xpath); + DEBUG("Incoming RIP query for xpath: %s", xpath); con = sr_session_get_connection(session); if (!con) { - ERROR("Error, getting sr connection"); + ERROR("Error getting sysrepo connection"); return SR_ERR_INTERNAL; } ctx = sr_acquire_context(con); if (!ctx) { - ERROR("Error, acquiring context"); + ERROR("Failed acquiring sysrepo context"); return SR_ERR_INTERNAL; } err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data for %s", yanger_args[1]); + ERROR("Failed adding yanger data for %s", yanger_args[1]); sr_release_context(con); @@ -325,19 +325,19 @@ static int sr_bfd_cb(sr_session_ctx_t *session, uint32_t, const char *, con = sr_session_get_connection(session); if (!con) { - ERROR("Error, getting sr connection"); + ERROR("Error getting sysrepo connection"); return SR_ERR_INTERNAL; } ctx = sr_acquire_context(con); if (!ctx) { - ERROR("Error, acquiring context"); + ERROR("Failed acquiring sysrepo context"); return SR_ERR_INTERNAL; } err = ly_add_yanger_data(ctx, parent, yanger_args); if (err) - ERROR("Error adding yanger data for %s", yanger_args[1]); + ERROR("Failed adding yanger data for %s", yanger_args[1]); sr_release_context(con); @@ -386,14 +386,14 @@ static int subscribe(struct statd *statd, char *model, char *xpath, SR_SUBSCR_DEFAULT | SR_SUBSCR_NO_THREAD | SR_SUBSCR_DONE_ONLY, &sub->sr_sub); if (err) { - ERROR("Error, subscribing to path \"%s\": %s", xpath, sr_strerror(err)); + ERROR("Failed subscribing to path \"%s\": %s", xpath, sr_strerror(err)); free(sub); return err; } err = sr_get_event_pipe(sub->sr_sub, &sr_ev_pipe); if (err) { - ERROR("Error, getting sysrepo event pipe: %s", sr_strerror(err)); + ERROR("Error getting sysrepo event pipe: %s", sr_strerror(err)); sr_unsubscribe(sub->sr_sub); free(sub); return err; From 60e02729e2ad6029bc58b9a80af77c3382a19692 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 2 Aug 2026 11:46:22 +0200 Subject: [PATCH 8/9] confd: fix dangling Finit symlinks for optional services Minimal images log the following on every boot and initctl reload, e.g., when the regression test framework reconfigures the system: finit[1]: Skipping /etc/finit.d/enabled/netbrowse.conf, dangling symlink: No such file or directory finit[1]: service_register():/etc/finit.d/enabled/ttyd.conf: skipping ttyd: No such file or directory finit[1]: Skipping /etc/finit.d/enabled/webui.conf, dangling symlink: No such file or directory The web services are enabled in the default configuration, so confd's finit_enable() creates enabled/ symlinks also on images where the service was never installed. svc_enable() already guards its nginx symlinks with the corresponding check. The ttyd case differs: its conf ships unconditionally in the common rootfs skeleton while the daemon itself is an optional package. Skip enable, with a log message at INFO, when the service conf is not available in the image. At build time, drop the ttyd confs when ttyd is not selected, and prune any dangling enabled/*.conf symlinks. Signed-off-by: Joachim Wiberg --- board/common/post-build.sh | 5 +++++ package/skeleton-init-finit/skeleton-init-finit.mk | 7 +++++++ .../skeleton}/etc/finit.d/available/ttyd.conf | 0 src/confd/src/core.c | 6 ++++++ 4 files changed, 18 insertions(+) rename {board/common/rootfs => package/skeleton-init-finit/skeleton}/etc/finit.d/available/ttyd.conf (100%) diff --git a/board/common/post-build.sh b/board/common/post-build.sh index eef3e9492..0135bc099 100755 --- a/board/common/post-build.sh +++ b/board/common/post-build.sh @@ -146,3 +146,8 @@ mkuserguide() if [ "$BR2_PACKAGE_WEBUI" = "y" ]; then mkuserguide fi + +# Drop dangling Finit enabled/*.conf symlinks, e.g., optional services +# not part of this image, they cause noise at every initctl reload. +# NOTE: must be the last step before creating the image! +find "$TARGET_DIR/etc/finit.d/enabled" -xtype l -delete 2>/dev/null diff --git a/package/skeleton-init-finit/skeleton-init-finit.mk b/package/skeleton-init-finit/skeleton-init-finit.mk index 618b174c9..8cc9f1000 100644 --- a/package/skeleton-init-finit/skeleton-init-finit.mk +++ b/package/skeleton-init-finit/skeleton-init-finit.mk @@ -254,6 +254,13 @@ endef SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_ULOGD endif +ifeq ($(BR2_PACKAGE_TTYD),y) +define SKELETON_INIT_FINIT_SET_TTYD + cp $(SKELETON_INIT_FINIT_AVAILABLE)/ttyd.conf $(FINIT_D)/available/ +endef +SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_TTYD +endif + ifeq ($(BR2_PACKAGE_WATCHDOGD),y) define SKELETON_INIT_FINIT_SET_WATCHDOGD cp $(SKELETON_INIT_FINIT_AVAILABLE)/watchdogd.conf $(FINIT_D)/available/ diff --git a/board/common/rootfs/etc/finit.d/available/ttyd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/ttyd.conf similarity index 100% rename from board/common/rootfs/etc/finit.d/available/ttyd.conf rename to package/skeleton-init-finit/skeleton/etc/finit.d/available/ttyd.conf diff --git a/src/confd/src/core.c b/src/confd/src/core.c index a0569b688..ac58c20a1 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -57,6 +57,12 @@ int finit_enable(const char *svc) (int)(at - svc), svc); } + if (!fexist(src)) { + /* Optional service not part of this image, avoid dangling symlink */ + INFO("%s is not available in this image, cannot enable", svc); + return 0; + } + snprintf(dst, sizeof(dst), FINIT_RCSD "/enabled/%s.conf", svc); if (symlink(src, dst) && errno != EEXIST) { ERRNO("failed enabling %s", svc); From 2d1a613e2a624bac63815366a6e42326e6975ffb Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 2 Aug 2026 23:23:11 +0200 Subject: [PATCH 9/9] confd: fix -v info and -v debug verbosity levels -v info mapped to LOG_NOTICE, making actual INFO level messages unreachable from the command line. -v debug opened the log mask but not the DEBUG() macro, which is gated on the debug variable, so debug messages still required the DEBUG environment variable to be set. Same behavior as statd. Signed-off-by: Joachim Wiberg --- src/confd/src/main.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/confd/src/main.c b/src/confd/src/main.c index f679d9df6..cb908623b 100644 --- a/src/confd/src/main.c +++ b/src/confd/src/main.c @@ -655,10 +655,11 @@ int main(int argc, char **argv) else if (!strcmp(optarg, "warning")) log_level = LOG_WARNING; else if (!strcmp(optarg, "info")) - log_level = LOG_NOTICE; - else if (!strcmp(optarg, "debug")) + log_level = LOG_INFO; + else if (!strcmp(optarg, "debug")) { log_level = LOG_DEBUG; - else { + debug = 1; + } else { fprintf(stderr, "confd error: Invalid verbosity \"%s\"\n", optarg); return EXIT_FAILURE; }