Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
Loading...
Searching...
No Matches
sapi_appmanager.c
Go to the documentation of this file.
1
13#define _POSIX_C_SOURCE 200809L
14
22
23#include <stdbool.h>
24#include <stdio.h>
25#include <stdlib.h>
26#include <string.h>
33
34/* sapi_appmanager_install_default_signal_handlers()'s POSIX detection -
35 * see this file's own implementation below and the function's doc in
36 * sapi_appmanager.h for why this is a scoped, documented exception to
37 * this module (and this framework)'s usual OS-agnosticism. */
45#if defined(__unix__) || defined(__APPLE__) || defined(__linux__)
46#define SAPI_APPMANAGER_HAVE_POSIX_SIGNALS 1
47#include <signal.h>
48#include <string.h>
49#else
50#define SAPI_APPMANAGER_HAVE_POSIX_SIGNALS 0
51#endif
52
56 .iteration_count = 0,
57 .error_count = 0,
58 .last_error = SAPI_STATUS_OK
59};
60
63static volatile int g_shutdown_requested = 0;
64
88 const char *stage_name,
89 uint32_t error_threshold)
90{
91 bool ok = (status == SAPI_STATUS_OK);
92
93 if (!ok) {
94 char msg[96];
95
97 g_app_state.last_error = status;
98
99 (void)snprintf(msg, sizeof(msg), "%s failed: %d", stage_name, (int)status);
100 sapi_log_write(SAPI_LOG_LEVEL_ERROR, "APPMANAGER", msg);
101
102 if (error_threshold > 0 && g_app_state.error_count >= error_threshold) {
103 (void)snprintf(msg, sizeof(msg), "Error threshold exceeded (%u/%u), shutting down",
104 g_app_state.error_count, error_threshold);
105 sapi_log_write(SAPI_LOG_LEVEL_ERROR, "APPMANAGER", msg);
107 }
108 }
109
110 return ok;
111}
112
122
130
141
150static void sapi_appmanager_encode_u64_le(uint8_t out[8], uint64_t value)
151{
152 uint32_t i;
153
154 for (i = 0U; i < 8U; i++) {
155 out[i] = (uint8_t)((value >> (8U * i)) & 0xFFU);
156 }
157}
158
160{
161 uint32_t low = (uint32_t)(signature & 0xFFFFFFFFULL);
162 uint32_t high = (uint32_t)((signature >> 32) & 0xFFFFFFFFULL);
163
164 return low ^ high;
165}
166
167/* ============================================================================
168 * Checkpoint marks (ADR-034)
169 * ========================================================================== */
170
171void sapi_appmanager_checkpoint_mark(const char *file, int32_t line, const char *label)
172{
173 char text[128];
174 uint8_t fold_buf[16];
175 sapi_crc64_t mark_hash;
176
178 /* Documented no-op outside an active sapi_appmanager_run() cycle -
179 * see this function's own header doc. */
180 return;
181 }
182
183 if (label != NULL) {
184 (void)snprintf(text, sizeof(text), "%s", label);
185 } else if (file != NULL) {
186 (void)snprintf(text, sizeof(text), "%s:%d", file, (int)line);
187 } else {
188 return;
189 }
190
191 mark_hash = sapi_checksum_crc64((const uint8_t *)text, strnlen(text, sizeof(text)));
192
194 sapi_appmanager_encode_u64_le(&fold_buf[8], (uint64_t)mark_hash);
195
196 g_checkpoint_signature = (uint64_t)sapi_checksum_crc64(fold_buf, sizeof(fold_buf));
197}
198
199/* ============================================================================
200 * Public API Implementation
201 * ========================================================================== */
202
204{
205 sapi_status_t status;
206
207 /* Validate configuration */
208 if (config == NULL || config->ops == NULL) {
209 sapi_log_write(SAPI_LOG_LEVEL_ERROR, "APPMANAGER", "Invalid sapi_appmanager_config_t");
210 return EXIT_FAILURE;
211 }
212
213 if (config->ops->init == NULL ||
214 config->ops->execute == NULL ||
215 config->ops->shutdown == NULL ||
216 config->ops->get_name == NULL ||
217 config->ops->get_version == NULL) {
218 sapi_log_write(SAPI_LOG_LEVEL_ERROR, "APPMANAGER", "Incomplete sapi_appmanager_operations_t");
219 return EXIT_FAILURE;
220 }
221
222 /* pre_execute/post_execute are optional (ADR-019); no NULL check here
223 * is an error - a NULL value simply means that stage is skipped below. */
224
225 /* REQ-APPMANAGER-009 (ADR-026): single-entry-point enforcement - this
226 * is THE one function that runs an application's lifecycle (see this
227 * function's own doc: "the single entry point for all applications"),
228 * so a call arriving while a PREVIOUS call is still mid-lifecycle
229 * (its own INITIALIZING/RUNNING/SHUTTING_DOWN) is refused outright,
230 * without touching any of g_app_state - unlike every other rejection
231 * path in this function, this one must NOT reset state out from under
232 * whichever call is already using it. Checked before the reset below
233 * runs, deliberately: this framework's own test suite (and any
234 * integrator) calling sapi_appmanager_run() again only AFTER a
235 * previous call has fully returned (state SHUTDOWN/ERROR by then) is
236 * unaffected - see sapi_lifecycle.h's own doc on why this remains a
237 * single-process-wide-instance model, not multi-instance. */
241 char msg[256];
242
243 (void)snprintf(msg, sizeof(msg),
244 "sapi_appmanager_run() called while an application is already running "
245 "(state=%d) - this is the single entry point, it cannot be re-entered concurrently",
246 (int)g_app_state.state);
247 sapi_log_write(SAPI_LOG_LEVEL_ERROR, "APPMANAGER", msg);
248 return EXIT_FAILURE;
249 }
250
251 /* Deliberately NOT validated here: config->checkpoint->voter being
252 * NULL at this point. An integrator may legitimately populate that
253 * target inside their own init() (e.g. a checkpoint transport whose
254 * channels are only registered as part of application startup, not
255 * before sapi_appmanager_run() is even called) - the per-cycle call
256 * below already handles a NULL voter safely (sapi_channel_checkpoint()
257 * returns SAPI_STATUS_INVALID_PARAM, handled identically to any other
258 * failed stage, never a crash), so an integrator can also toggle it
259 * to NULL transiently at runtime to pause checkpointing (e.g. while
260 * its own underlying transport is known down) without that ever
261 * being treated as a startup error. */
262
263 /* Initialize application manager state */
269 /* ADR-034: no cycle is active yet - any SAPI_CHECKPOINT_MARK() called
270 * from init() is a documented no-op (see sapi_appmanager_checkpoint_mark()'s
271 * own doc), not a fold into whatever this run's first real cycle
272 * computes. */
274 /* REQ-APPMANAGER-010 (ADR-026): setup is allowed again for this (new) call's own INIT
275 * phase - see sapi_lifecycle.h's own doc on why this reset lives here
276 * rather than only at the end of the previous call. */
278
279 /* Startup banner */
280 {
281 char msg[96];
282
283 (void)snprintf(msg, sizeof(msg), "%s v%s starting", config->ops->get_name(), config->ops->get_version());
284 sapi_log_write(SAPI_LOG_LEVEL_INFO, "APPMANAGER", msg);
285 }
286
287 /* ========================================================================
288 * INITIALIZATION PHASE
289 * ======================================================================== */
290
291 status = config->ops->init(config->context);
292
293 if (status != SAPI_STATUS_OK) {
294 char msg[64];
295
296 (void)snprintf(msg, sizeof(msg), "Initialization failed: %d", (int)status);
297 sapi_log_write(SAPI_LOG_LEVEL_ERROR, "APPMANAGER", msg);
299 g_app_state.last_error = status;
301
302 /* Always attempt shutdown even on init failure */
303 config->ops->shutdown(config->context);
304
305 return EXIT_FAILURE;
306 }
307
309 /* REQ-APPMANAGER-010 (ADR-026): setup phase locked - every setup-only constructor
310 * (sapi_timer_create(), sapi_channel_init(), sapi_voter_init()/
311 * _register_channel(), sapi_cross_comparator_init()/
312 * _register_channel(), sapi_watchdog_create()) now rejects with
313 * SAPI_STATUS_INVALID_STATE for the rest of this run - see
314 * sapi_lifecycle.h's own doc for the deliberately-excluded
315 * netlink/dual reconnect exceptions. */
317
318 /* ========================================================================
319 * EXECUTION PHASE
320 * ======================================================================== */
321
322 while (!g_shutdown_requested &&
323 (config->max_iterations == 0 ||
325
327
328 /* ADR-034: reset this cycle's checkpoint-mark signature before any
329 * of this cycle's own work runs, so SAPI_CHECKPOINT_MARK() calls
330 * made during pre_execute()/execute()/post_execute() below fold
331 * into a signature that represents ONLY this cycle's own path -
332 * see sapi_appmanager_checkpoint_mark()'s own doc. */
334
335 /* Stage 1 (ADR-019, optional): per-cycle input/prepare stage. */
336 if (config->ops->pre_execute != NULL) {
337 status = config->ops->pre_execute(config->context);
338 if (!sapi_appmanager_handle_stage_result(status, "pre_execute", config->error_threshold)) {
339 continue;
340 }
341 }
342
343 /* Stage 2 (mandatory): main application logic, unchanged. */
344 status = config->ops->execute(config->context);
345 if (!sapi_appmanager_handle_stage_result(status, "execute", config->error_threshold)) {
346 continue;
347 }
348
349 /* Stage 3 (ADR-019, optional): per-cycle output/cleanup stage. Only
350 * reached once execute() itself succeeded. */
351 if (config->ops->post_execute != NULL) {
352 status = config->ops->post_execute(config->context);
353 (void)sapi_appmanager_handle_stage_result(status, "post_execute", config->error_threshold);
354 }
355
356 /* Stage 4 (ADR-019/ADR-034, optional): bounded cross-channel
357 * checkpoint rendezvous, run LAST - after pre_execute()/execute()/
358 * post_execute() have all had a chance to fold their own marks
359 * into this cycle's signature (SAPI_CHECKPOINT_MARK()) - using
360 * that folded signature as the checkpoint_id. See
361 * sapi_appmanager_checkpoint_config_t's own doc for why this
362 * replaced the old iteration_count-based scheme and the old
363 * before-everything stage position.
364 *
365 * REQ-APPMANAGER-011: config->checkpoint->voter == NULL is
366 * deliberately treated the SAME as config->checkpoint == NULL -
367 * skip the rendezvous itself - not as a failed checkpoint attempt.
368 * An integrator may legitimately toggle voter to NULL at runtime
369 * to pause checkpointing while its own underlying transport is
370 * known down (see sapi_appmanager_config_t's own doc).
371 *
372 * committed starts true: with no checkpoint configured (or paused),
373 * ops->on_checkpoint_result() - if registered - is still called,
374 * with committed=true (see that field's own doc: an application
375 * using stage-then-commit should not have to special-case whether
376 * checkpointing happened to be active this cycle). */
377 {
378 bool committed = true;
379
380 if ((config->checkpoint != NULL) && (config->checkpoint->voter != NULL)) {
381 sapi_checkpoint_config_t checkpoint_cfg;
382
384 checkpoint_cfg.max_delay_ms = config->checkpoint->max_delay_ms;
385 checkpoint_cfg.expected_node_count = config->checkpoint->expected_node_count;
386 checkpoint_cfg.watchdog = config->checkpoint->watchdog;
387
388 status = sapi_channel_checkpoint(config->checkpoint->voter, &checkpoint_cfg);
389 committed = sapi_appmanager_handle_stage_result(status, "checkpoint", config->error_threshold);
390 /* Deliberately no pacing/continue on failure here (unlike
391 * every other stage above): pre_execute() already ran this
392 * cycle - now that checkpoint runs LAST, a failed
393 * checkpoint no longer starves it, so the pacing workaround
394 * this stage used to need (see git history) is gone. Falls
395 * through to on_checkpoint_result below regardless, then
396 * the loop naturally continues to its own top. */
397 }
398
399 if (config->ops->on_checkpoint_result != NULL) {
400 status = config->ops->on_checkpoint_result(config->context, committed);
401 (void)sapi_appmanager_handle_stage_result(status, "on_checkpoint_result", config->error_threshold);
402 }
403 }
404 }
405
406 /* ========================================================================
407 * SHUTDOWN PHASE
408 * ======================================================================== */
409
411 /* ADR-034: the cycle loop has exited - any SAPI_CHECKPOINT_MARK() called
412 * from shutdown() is a documented no-op (see
413 * sapi_appmanager_checkpoint_mark()'s own doc). */
415 /* REQ-APPMANAGER-010 (ADR-026): setup is allowed again from here on - not just at the next
416 * call's own top-of-run reset above. Without this, setup code that
417 * legitimately runs BETWEEN two sapi_appmanager_run() calls (e.g. a
418 * test fixture - or integrator code - that builds the NEXT run's own
419 * voter/channels/timers before calling sapi_appmanager_run() again)
420 * would be incorrectly rejected: it runs after this run's own
421 * sapi_lifecycle_lock() but before the next run's own unlock. */
423
424 status = config->ops->shutdown(config->context);
425
426 if (status != SAPI_STATUS_OK) {
427 char msg[64];
428
429 (void)snprintf(msg, sizeof(msg), "Shutdown returned non-OK status: %d", (int)status);
430 sapi_log_write(SAPI_LOG_LEVEL_WARNING, "APPMANAGER", msg);
431 /* Continue anyway; shutdown must complete */
432 }
433
435
436 /* Final summary - the iteration count is this event's natural Cycle
437 * field, so this uses sapi_log_write_event() (structured), not
438 * sapi_log_write() like this function's other, cycle-less messages
439 * above. */
440 {
441 char extra[64];
442
443 (void)snprintf(extra, sizeof(extra), "Errors=%u FinalState=%s", g_app_state.error_count,
444 (g_app_state.error_count > 0 ? "ERROR" : "OK"));
445 sapi_log_write_event(SAPI_LOG_LEVEL_INFO, "", g_app_state.iteration_count, "APPMANAGER", "-", "SUMMARY",
446 config->ops->get_name(), extra);
447 }
448
449 /* Determine exit code */
450 if (g_app_state.error_count > 0 && config->error_threshold > 0) {
451 return EXIT_FAILURE;
452 }
453
454 return EXIT_SUCCESS;
455}
456
461
463{
464 if (state == NULL) {
466 }
467
468 *state = g_app_state;
469 return SAPI_STATUS_OK;
470}
471
476
487
488#if SAPI_APPMANAGER_HAVE_POSIX_SIGNALS
489
490/* Async-signal-safe: writes one volatile int, nothing else (no I/O, no
491 * allocation) - see sapi_appmanager_request_shutdown()'s own definition
492 * above. */
493static void sapi_appmanager_signal_handler(int signum)
494{
495 (void)signum;
497}
498
500{
501 struct sigaction sa;
502
503 /* sigemptyset()/sigaction() failure paths below (each GCOVR_EXCL_LINE)
504 * are checked defensively per this project's error-handling
505 * convention, but neither call has a documented failure mode for the
506 * fixed, always-valid arguments used here (a stack-local sigset_t;
507 * SIGINT/SIGTERM, both always-valid, unblockable-by-definition signal
508 * numbers) - forcing a real failure would need OS-level fault
509 * injection (e.g. LD_PRELOAD interposition), not something a portable
510 * unit test can do. */
511 memset(&sa, 0, sizeof(sa));
512 sa.sa_handler = sapi_appmanager_signal_handler;
513 if (sigemptyset(&sa.sa_mask) != 0)
514 {
515 return SAPI_STATUS_INTERNAL_ERROR; /* GCOVR_EXCL_LINE */
516 }
517 sa.sa_flags = 0;
518
519 if (sigaction(SIGINT, &sa, NULL) != 0)
520 {
521 return SAPI_STATUS_INTERNAL_ERROR; /* GCOVR_EXCL_LINE */
522 }
523 if (sigaction(SIGTERM, &sa, NULL) != 0)
524 {
525 return SAPI_STATUS_INTERNAL_ERROR; /* GCOVR_EXCL_LINE */
526 }
527 return SAPI_STATUS_OK;
528}
529
530#else
531
536
537#endif /* SAPI_APPMANAGER_HAVE_POSIX_SIGNALS */
void sapi_appmanager_request_shutdown(void)
Request application shutdown.
sapi_app_state_t sapi_appmanager_get_state(void)
Get current application state.
sapi_status_t sapi_appmanager_get_stats(sapi_appmanager_state_t *state)
Get application statistics.
uint32_t sapi_appmanager_checkpoint_fold_signature(uint64_t signature)
Pure helper: folds a 64-bit checkpoint signature down to the uint32_t sapi_checkpoint_config_t::check...
sapi_app_state_t
Application state enumeration.
int sapi_appmanager_run(const sapi_appmanager_config_t *config)
Run application with lifecycle management.
void sapi_appmanager_reset_state(void)
Forcibly resets the application manager's own bookkeeping (lifecycle state, iteration/error counters,...
#define SAPI_APPMANAGER_CHECKPOINT_SIGNATURE_SEED
Reset value of the per-cycle checkpoint signature before any marks are folded into it (ADR-034) - see...
sapi_status_t sapi_appmanager_install_default_signal_handlers(void)
POSIX-only convenience: installs SIGINT and SIGTERM handlers that call sapi_appmanager_request_shutdo...
void sapi_appmanager_checkpoint_mark(const char *file, int32_t line, const char *label)
Folds a hash of this call site into the current cycle's checkpoint signature (ADR-034).
@ SAPI_APP_STATE_ERROR
@ SAPI_APP_STATE_RUNNING
@ SAPI_APP_STATE_SHUTDOWN
@ SAPI_APP_STATE_SHUTTING_DOWN
@ SAPI_APP_STATE_INITIALIZING
@ SAPI_APP_STATE_UNINITIALIZED
sapi_status_t sapi_channel_checkpoint(sapi_voter_t *voter, const sapi_checkpoint_config_t *config)
Performs one bounded checkpoint rendezvous across every channel registered with a voter.
uint64_t sapi_crc64_t
CRC-64 checksum value (64-bit).
sapi_crc64_t sapi_checksum_crc64(const uint8_t *data, size_t size)
Compute CRC-64 for data buffer.
void sapi_lifecycle_lock(void)
Locks the application's setup phase.
void sapi_lifecycle_unlock(void)
Unlocks the application's setup phase (setup is allowed again).
void sapi_log_write_event(sapi_log_level_t level, const char *site, uint32_t cycle, const char *source, const char *destination, const char *type, const char *info, const char *extra_fields)
Emits one structured inter-channel event/message-trail log line - for a message actually sent/receive...
Definition sapi_log.c:188
void sapi_log_write(sapi_log_level_t level, const char *tag, const char *message)
Emits one log message. Non-blocking; never fails the caller's control flow even if the message is dro...
Definition sapi_log.c:108
@ SAPI_LOG_LEVEL_ERROR
Definition sapi_log.h:38
@ SAPI_LOG_LEVEL_INFO
Definition sapi_log.h:36
@ SAPI_LOG_LEVEL_WARNING
Definition sapi_log.h:37
sapi_status_t
Common result/status codes.
Definition sapi_status.h:27
@ SAPI_STATUS_INTERNAL_ERROR
Definition sapi_status.h:38
@ SAPI_STATUS_NOT_SUPPORTED
Definition sapi_status.h:34
@ SAPI_STATUS_INVALID_PARAM
Definition sapi_status.h:29
@ SAPI_STATUS_OK
Definition sapi_status.h:28
static bool g_checkpoint_signature_active
True once g_checkpoint_signature has been reset for the CURRENT sapi_appmanager_run() cycle - see sap...
static void sapi_appmanager_checkpoint_signature_reset(void)
Resets g_checkpoint_signature to a fixed seed and marks it active for the current cycle - called once...
static uint64_t g_checkpoint_signature
Running per-cycle checkpoint-mark signature (ADR-034) - see sapi_appmanager_checkpoint_mark()'s own d...
static bool sapi_appmanager_handle_stage_result(sapi_status_t status, const char *stage_name, uint32_t error_threshold)
Records the outcome of one per-cycle stage (checkpoint, pre_execute, execute, or post_execute) agains...
static void sapi_appmanager_encode_u64_le(uint8_t out[8], uint64_t value)
Packs a uint64_t into an 8-byte buffer, explicit little-endian - same convention sapi_checkpoint....
static sapi_appmanager_state_t g_app_state
Global application state (single instance; no dynamic allocation).
static volatile int g_shutdown_requested
Set by the installed signal handler (or sapi_appmanager_request_shutdown()) to request that sapi_appm...
Application Manager abstraction for safeAPIFramework applications.
Bounded checkpoint rendezvous for distributed vital channels (ADR-017).
Checksum and CRC utilities for data integrity in redundant systems.
Process-wide application setup-phase lock (ADR-026).
OS Abstraction Layer - Logging/diagnostics service.
OS Abstraction Layer - Timer service.
Application manager configuration.
const sapi_appmanager_checkpoint_config_t * checkpoint
const sapi_appmanager_operations_t * ops
sapi_status_t(*) init(void *context)
Initialize application.
sapi_status_t(*) on_checkpoint_result(void *context, bool committed)
Optional per-cycle checkpoint outcome hook (ADR-034).
sapi_status_t(*) execute(void *context)
Execute main application logic.
sapi_status_t(*) shutdown(void *context)
Shutdown application.
const char *(*) get_name(void)
Get human-readable application name.
sapi_status_t(*) pre_execute(void *context)
Optional per-cycle input/prepare stage (ADR-019).
sapi_status_t(*) post_execute(void *context)
Optional per-cycle output/cleanup stage (ADR-019).
const char *(*) get_version(void)
Get application version string.
Application manager runtime state.
Checkpoint configuration.
sapi_duration_ms_t max_delay_ms