diff --git a/app/libsystemd/docs/api-reference.md b/app/libsystemd/docs/api-reference.md index 67fb9a9..269cdc9 100644 --- a/app/libsystemd/docs/api-reference.md +++ b/app/libsystemd/docs/api-reference.md @@ -78,7 +78,7 @@ Looks `unit_name` up in the global registry and starts it. For a `simple`/ `Dmod_SpawnModule` so the unit's lifetime is never tied to whichever process happened to call this). For a `type=library` unit, instead loads and enables `exec` as a Library module (`Dmod_LoadModuleByName` + `Dmod_EnableModule`) - -no process is spawned; see [configuration.md](configuration.md#typelibrary-services-backed-by-a-library-module-not-a-process). +no process is spawned; see [configuration.md](configuration.md#typelibrary-a-service-backed-by-a-library-module). If `unit_name` is not already registered but is `@`-shaped and `@.ini` exists in the last-scanned units directory, it is @@ -87,7 +87,9 @@ instantiated on the fly and added to the registry before being spawned - see This is the only place template instantiation happens outside of `libsystemd_scan()`/`libsystemd_parse_dir()`. `user_value` (may be `NULL`) is substituted for `%v` in the template's keys during that on-demand -instantiation only - it has no effect if `unit_name` is already registered. +instantiation. An inactive library template instance is also refreshed when +a non-NULL value is supplied again. Active units and concrete units retain +their current arguments. If the unit's `restart` key is not `no`, also registers a `dmosi` process-exit callback on the newly spawned process (best-effort - silently skipped if @@ -108,7 +110,10 @@ For a `type=library` unit: - `-EINVAL` - `unit_name` was `NULL`. - `-ENOENT` - no unit with that name, and it could not be instantiated from a template either; or the module could not be found/loaded. -- `-EALREADY` - the module is already enabled. +- `-EALREADY` - the callback-backed unit is already active, or a plain library + module is already enabled. +- `-ENOTSUP` - only one of the paired lifecycle callbacks is implemented. +- Other callback errors propagate; a failed START leaves the unit inactive. - `-EIO` - the module was loaded but could not be enabled (e.g. it is not a Library module, or a required module could not be enabled). @@ -123,14 +128,16 @@ semaphore posted, and it gets up to the unit's `stop_timeout_ms` to exit on its own - the call blocks meanwhile, but never longer, whatever the service does. Otherwise, or if it does not exit in time, it is killed (`dmosi_process_kill`). See [Graceful stop](configuration.md#graceful-stop). For a `type=library` unit, instead -disables then unloads `exec` (`Dmod_DisableModule` + `Dmod_UnloadModule`) - -disable before unload, since the dmod core refuses to unload a module that is -still enabled. +calls its STOP callback when the module implements the lifecycle DIF. Only +that unit becomes inactive; shared module code stays enabled. Callback errors +propagate and preserve active state. Plain libraries instead disable then +unload `exec` (`Dmod_DisableModule` + `Dmod_UnloadModule`). - `-EINVAL` - `unit_name` was `NULL`. - `-ENOENT` - no unit with that name. - `-ESRCH` - the unit exists but has no running process to stop (or, for a - `type=library` unit, is neither loaded nor enabled). + `type=library` unit, its callback-backed instance is inactive or its plain + module is neither loaded nor enabled). - `-EIO` - (`type=library` only) disabling or unloading the module failed. ### `libsystemd_notify_main_pid(const char* unit_name, Dmod_Pid_t pid)` @@ -188,10 +195,10 @@ Fills `*out_status` with the unit's current process state and PID. Reports can no longer be found (e.g. it exited on its own). For a `type=library` unit there is no process/PID at all - `pid` is always -`0`, and `state` is read live from the module's own state (`Dmod_IsModuleEnabled`): -`DMOSI_PROCESS_STATE_RUNNING` while enabled, `DMOSI_PROCESS_STATE_CREATED` -otherwise (covering both "never started" and "stopped" - unlike a -process-backed unit there is no tracked "last" identity to tell those apart). +`0`. For callback-backed modules, `state` is RUNNING only while this unit has +a successful START and the module remains enabled. Otherwise it is CREATED. +For plain libraries, RUNNING follows `Dmod_IsModuleEnabled`; stopped and +never-started instances both report CREATED. - `-EINVAL` - `unit_name`/`out_status` was `NULL`. - `-ENOENT` - no unit with that name. @@ -255,3 +262,11 @@ by a later `libsystemd_scan()`/`libsystemd_load_rules()` replay. with the resolved name is currently registered. - `-ESRCH` - the resolved unit exists but has no running process to stop. - `-ENOMEM` - allocation failed while resolving the target. + +## Library-service instance lifecycle + +The enable/disable library lifecycle described above applies to plain libraries. +Modules implementing both DIF callbacks in `libsystemd_service.h` instead receive +per-unit START/STOP with expanded arguments. Unit state is independent of an +already-enabled shared module; stop does not disconnect other API consumers. +See [configuration](configuration.md#typelibrary-a-service-backed-by-a-library-module). diff --git a/app/libsystemd/docs/configuration.md b/app/libsystemd/docs/configuration.md index 1c565b3..e60618d 100644 --- a/app/libsystemd/docs/configuration.md +++ b/app/libsystemd/docs/configuration.md @@ -61,54 +61,56 @@ never tied to whichever process happened to start it - see the comment in `library` is a different kind of unit altogether - see the next section. -#### `type=library`: services backed by a Library module, not a process - -`simple`/`oneshot` units both assume `exec` names an **Application**-type -DMOD module - one with a `main()` that can be spawned as a process. Not every -piece of functionality that needs to be started/stopped by `libsystemd` is -shaped like that: a **Library** module (no `main()`, e.g. a protocol handler -that does its job entirely from `dmod_init()`/`dmod_deinit()` once -loaded/enabled - see -[`dmicmp`](https://github.com/choco-technologies/dmicmp)) can't be spawned at -all, only loaded and enabled into the running DMOD system. - -`type=library` covers exactly this case (named after `Dmod_ModuleType_Library` -in the dmod core, the same vocabulary DMOD itself uses for the two kinds of -module): `exec` names the Library module instead of an executable, and -starting/stopping the unit maps to the DMOD loader's own module lifecycle -instead of a process one: - -- **start** - loads the module if it is not already loaded - (`Dmod_LoadModuleByName`), then enables it (`Dmod_EnableModule`). Enabling - is what actually runs the module's `dmod_init()` and pulls in/enables its - required modules - a module left merely loaded never does anything (see the - dmod core's `Dmod_Enable`). -- **stop** - disables the module (`Dmod_DisableModule`), then unloads it - (`Dmod_UnloadModule`) - disable before unload, since the dmod core refuses - to unload a module that is still enabled. - -Because there is no process, `args`/`stdin`/`stdout`/`stderr`/`stdlog` have no -effect on a `library`-type unit, and `restart` supervision does not apply -either (there is no process exit to trigger it) - only `after`/`requires` -ordering behaves the same as for any other unit. `libsystemd_status()`/ -`service status` still work: they report `DMOSI_PROCESS_STATE_RUNNING` -(`pid=0`) while the module is enabled and `DMOSI_PROCESS_STATE_CREATED` -(`pid=0`) otherwise, read live from the module's own state -(`Dmod_IsModuleEnabled`) rather than from any state `libsystemd` tracks -itself. +#### `type=library`: a service backed by a Library module + +`exec` names a Library module. No process is spawned and status uses PID zero. +For plain libraries, start loads/enables the module and stop disables/unloads +it. For example: ```ini -# icmp.ini -description=Answer ICMP Echo Requests (ping) exec=dmicmp type=library ``` -`Dmod_EnableModule`/`Dmod_LoadModuleByName` (and their `Unload`/`Disable` -counterparts) reject anything that is not a Library module, so pointing -`type=library` at an Application-type module's name simply fails to start (see -`libsystemd_start_service()`'s return codes) rather than doing something -unexpected. +Shared libraries can implement the paired lifecycle DIF from +`libsystemd_service.h`. In that case module initialization and service instance +lifetime are separate: + +- START enables the module if necessary, then calls `service_start(unit, argc, + argv)` with the fully expanded `exec`/`args` vector. This still happens when + API consumers enabled the module earlier. Each unit has its own active state. +- STOP calls `service_stop(unit)`. Success clears only that unit's active state; + failure leaves it active. Other units and API consumers retain shared code. + Disabling/unloading the module remains a module-lifecycle operation. +- Implement both callbacks or neither; an incomplete pair fails with ENOTSUP. + Callback arguments are borrowed for the call; copy anything retained. +- Callback-backed status is RUNNING only after successful START and while the + module remains enabled. A loaded/enabled library alone is not a running unit. + +An RTC device rule can therefore start the UTC library with the reported path: + +```ini +; dmutc@.ini +exec=dmutc +type=library +args=%v +``` + +```ini +; dmutc.rules +[class=rtc] +start=dmutc@%name +``` + +Here `%v` is the device event's actual path. Multiple RTCs produce separate +unit instances using one shared module. Device removal stops the corresponding +instance. A stopped template is re-expanded when a re-added device supplies a +new `%v`; its previous path is not reused. Devices remembered before rules load +are replayed through the same START path. No application shim or hardcoded device path is needed. + +Stream redirection does not apply to library units. `args` is ignored for plain +libraries without lifecycle DIF. Process restart supervision does not apply; +`after`/`requires` ordering remains available. A non-Library `exec` fails enable. ### Restart supervision diff --git a/app/libsystemd/docs/library-service-validation.md b/app/libsystemd/docs/library-service-validation.md new file mode 100644 index 0000000..eb1a619 --- /dev/null +++ b/app/libsystemd/docs/library-service-validation.md @@ -0,0 +1,46 @@ +# Library lifecycle validation — 2026-10-16 + +- Native libsystemd diagnostic: 63/63, including cold loading a DIF-only + library, a module already enabled through an API dependency, two unit + instances sharing one module, failed starts, status, removal/re-add and + refreshed %v paths. The additional plain-library test performs two full load/enable, + duplicate-START, disable/unload and restart cycles without lifecycle DIF. +- Native systemd and service command diagnostics: 1/1 each. +- Full Cortex-M7 build passed, including both lifecycle helper modules. +- STM32F746G-DISCO integration with dmdevfs and dmutc: the RTC event starts + dmutc@rtc automatically after flash/reset with type=library and pid=0. + dmutc diagnostic 6/6, RTC 12/12 and RNG 6/6. The UTC test stops/restarts a + library unit without unloading API consumers and removes/re-adds the RTC + with both an invalid new path and the actual path. + +The cold-load test is separate from the API-imported helper: DIF lookup must +happen after enabling the library. The changed-path test ensures a retained +inactive template instance expands the new event value instead of keeping +an old device path. + +Board console evidence is maintained in the dmutc PR alongside its diagnostic. + +## Complete boot integration regression + +The dmod-boot CI Renode runner passed console/VFS, SD identification and +automount, read/write across unmount/remount, LCD splash/selftest/fill/bars/ +gradient and touch coordinates/watch tests. A second run with the board audio +models enabled also passed those checks. The host lacks root and /dev/net/tun, +so emulated network testing was skipped explicitly; physical board DHCP, +board-to-router ping, host-to-board ping and telnet command execution passed. +The physical board also rejected stopping the plain ICMP library while dmudp +requires it, preserving the running service and ping responses. + +The boot integration results are summarized in +https://github.com/choco-technologies/dmod-boot/pull/132. +Raw board and emulator logs are retained locally rather than published. + +With the emulator audio models enabled, a one-second 523 Hz tone plays with +zero underruns. Captured stereo PCM has equal channels and a measured frequency +of about 522 Hz by zero crossings, consistent with the requested tone and the +measurement window. This checks signal generation, not clock calibration. + +Network qualification: fresh host-to-board ping sessions lose their first +request (extended probe: 9/10), while the gateway control probe is 3/3. The +cause remains unresolved; the board network checks do not establish loss-free +operation. The skipped Renode TAP test also remains a validation limitation. diff --git a/app/libsystemd/include/libsystemd_service.h b/app/libsystemd/include/libsystemd_service.h new file mode 100644 index 0000000..917f21f --- /dev/null +++ b/app/libsystemd/include/libsystemd_service.h @@ -0,0 +1,23 @@ +#ifndef LIBSYSTEMD_SERVICE_H +#define LIBSYSTEMD_SERVICE_H +#include "dmod.h" +#include "libsystemd_defs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Optional paired lifecycle DIF for shared Library modules. dmod_init/deinit + * own module lifetime; these callbacks own each unit instance. argc/argv are + * the fully expanded exec/args values (argv[0] is exec). Strings are borrowed + * for the duration of the call. START failure must leave that unit inactive; + * STOP failure leaves it active and may be retried. No process is spawned. + * Modules without either callback retain the legacy enable/disable behavior. + * A callback-backed stop does not unload shared code or disconnect consumers. */ +dmod_libsystemd_dif(1.0, int, _service_start, + (const char* unit_name, int argc, const char* const argv[])); +dmod_libsystemd_dif(1.0, int, _service_stop, (const char* unit_name)); +#ifdef __cplusplus +} +#endif +#endif diff --git a/app/libsystemd/include/libsystemd_types.h b/app/libsystemd/include/libsystemd_types.h index 4d4da23..1d3faef 100644 --- a/app/libsystemd/include/libsystemd_types.h +++ b/app/libsystemd/include/libsystemd_types.h @@ -26,7 +26,7 @@ typedef enum { LIBSYSTEMD_SERVICE_TYPE_SIMPLE, //!< Default. The process is expected to keep running until stopped; an unexpected exit is logged as a warning. LIBSYSTEMD_SERVICE_TYPE_ONESHOT, //!< The process is expected to run to completion and exit on its own; a clean (status 0) exit is not treated as a crash. - LIBSYSTEMD_SERVICE_TYPE_LIBRARY, //!< "exec" names a Library-type DMOD module (see ::Dmod_ModuleType_Library), not a spawnable Application: starting the unit loads and enables that module, stopping it disables and unloads it - no process is ever spawned. + LIBSYSTEMD_SERVICE_TYPE_LIBRARY, //!< "exec" names a Library-type DMOD module (see ::Dmod_ModuleType_Library), not a spawnable Application: plain units enable/disable it, while optional service lifecycle DIF callbacks manage each unit instance without unloading shared code. No process is spawned. } libsystemd_service_type_t; /** diff --git a/app/libsystemd/src/serviceapi.c b/app/libsystemd/src/serviceapi.c index ecb58f0..cda9d23 100644 --- a/app/libsystemd/src/serviceapi.c +++ b/app/libsystemd/src/serviceapi.c @@ -1,5 +1,7 @@ #define DMOD_ENABLE_REGISTRATION ON +#define ENABLE_DIF_REGISTRATIONS #include "libsystemd.h" +#include "libsystemd_service.h" #include #include #include "dmini.h" @@ -45,6 +47,8 @@ struct libsystemd_service libsystemd_service_type_t type; //!< From the optional "type" key ("simple"/"oneshot"). Defaults to ::LIBSYSTEMD_SERVICE_TYPE_SIMPLE. libsystemd_restart_policy_t restart_policy; //!< From the optional "restart" key ("no"/"always"/"on-failure"). Defaults to ::LIBSYSTEMD_RESTART_NO. uint32_t stop_timeout_ms; //!< From the optional "stop_timeout_ms" key: how long a stop waits for a service registered for stop requests to exit on its own before killing it. 0 = kill at once. Defaults to ::LIBSYSTEMD_DEFAULT_STOP_TIMEOUT_MS. + bool from_template; // Allows a stopped device unit to receive a fresh event value. + bool library_started; // Callback-backed unit instance, independent of module enabled state. char* exec; //!< Module name (or file path) to spawn, from the "exec" key. Owned copy. int argc; //!< Number of entries in argv (always >= 1, argv[0] == exec). char** argv; //!< NULL-terminated argument vector (argc+1 entries, each an owned copy). @@ -887,107 +891,71 @@ static void libsystemd_detach_exit_callback(libsystemd_service_t service, dmosi_ } /** - * @brief Actually start a ::LIBSYSTEMD_SERVICE_TYPE_LIBRARY unit, without looking it up by name first - * - * Counterpart of libsystemd_start_service_internal() for a unit whose `exec` - * names a Library-type DMOD module rather than a spawnable Application: - * "starting" it means loading and enabling that module - * (`Dmod_LoadModuleByName()` + `Dmod_EnableModule()`), not spawning a - * process. No `argc`/`argv`/stream redirection applies (those are meaningless - * for a module that is never run as a process), and there is no PID to track - * or restart-supervision callback to attach - libsystemd_fill_status() reads - * the module's live loaded/enabled state directly (via `Dmod_IsModuleLoaded()`/ - * `Dmod_IsModuleEnabled()`) instead of a tracked `pid`. - * - * `Dmod_LoadModuleByName()` is itself idempotent for a Library module already - * loaded (returns the existing context rather than failing - see the dmod - * core), so this only calls it when the module is not already loaded; a - * module left loaded but not enabled by some earlier partial failure is still - * picked up and enabled here rather than treated as "already started". If - * enabling fails and this call is the one that loaded the module (it was not - * already loaded beforehand), the load is undone before returning - most - * commonly hit by pointing `type=library` at an Application-type module by - * mistake: loading it still succeeds (the dmod core only enforces "Library - * only" on enable), but leaving it loaded would leak a fresh, unshared - * Application context on every failed start (see `Dmod_LoadModuleByName()` in - * the dmod core - unlike a Library module, an already-loaded Application - * module is never reused). - * - * @param service Service to start (must not be NULL); `service->exec` is the module name. - * - * @retval 0 The module is now loaded and enabled. - * @retval -EALREADY The module was already enabled. - * @retval -ENOENT The module could not be found/loaded. - * @retval -EIO The module was loaded but `Dmod_EnableModule()` failed - * (e.g. it is not a Library module, or one of its required - * modules could not be enabled). + * Start a Library unit. Modules implementing the paired service START/STOP DIF + * are enabled once and receive expanded argv per unit instance. Module enable + * alone does not mean a callback-backed unit is running; API dependencies may + * have enabled the library before its device arrived. Plain libraries keep + * the original load/enable lifecycle and EALREADY behavior. */ static int libsystemd_start_library_service_internal(libsystemd_service_t service) { - if (Dmod_IsModuleEnabled(service->exec)) - { - return -EALREADY; - } - - DMOD_LOG_INFO("Loading library service '%s' (module '%s')\n", service->unit_name, service->exec); - + bool enabled = Dmod_IsModuleEnabled(service->exec); bool already_loaded = Dmod_IsModuleLoaded(service->exec); - if (!already_loaded && Dmod_LoadModuleByName(service->exec) == NULL) - { - return -ENOENT; - } - - if (!Dmod_EnableModule(service->exec, false, NULL)) - { - if (!already_loaded) - { - Dmod_UnloadModule(service->exec, true); - } + if (!already_loaded && Dmod_LoadModuleByName(service->exec) == NULL) return -ENOENT; + // DIF resolution is only valid on an enabled module. The very first + // device event must work, not just a retry after another API enabled it. + if (!Dmod_EnableModule(service->exec, false, NULL)) { + if (!already_loaded) Dmod_UnloadModule(service->exec, true); return -EIO; } - + Dmod_Context_t* module = Dmod_GetModuleContext(service->exec); + dmod_libsystemd_service_start_t start = Dmod_GetDifFunction(module, dmod_libsystemd_service_start_sig); + dmod_libsystemd_service_stop_t stop = Dmod_GetDifFunction(module, dmod_libsystemd_service_stop_sig); + if ((start == NULL) != (stop == NULL)) { + if (!already_loaded && Dmod_DisableModule(service->exec, false)) + Dmod_UnloadModule(service->exec, false); + return -ENOTSUP; + } + if (start == NULL) return enabled ? -EALREADY : 0; + if (service->library_started && enabled) return -EALREADY; + service->library_started = false; + int rc = start(service->unit_name, service->argc, (const char* const*)service->argv); + if (rc != 0) return rc; + service->library_started = true; return 0; } /** - * @brief Actually stop a ::LIBSYSTEMD_SERVICE_TYPE_LIBRARY unit, without looking it up by name first - * - * Counterpart of libsystemd_stop_service_internal() for a library-type unit: - * "stopping" it means disabling and then unloading the module - * (`Dmod_DisableModule()` + `Dmod_UnloadModule()`), the reverse order of - * libsystemd_start_library_service_internal() - `Dmod_Unload()` in the dmod - * core refuses to unload a module that is still enabled (see `dmod_system.c`), - * so disabling first is required, not just symmetrical. - * - * @param service Service to stop (must not be NULL); `service->exec` is the module name. - * - * @retval 0 The module is now disabled and unloaded (or was already unloaded entirely). - * @retval -ESRCH The module was neither loaded nor enabled - nothing to stop. - * @retval -EIO `Dmod_DisableModule()`/`Dmod_UnloadModule()` failed (e.g. another - * still-enabled module requires this one). + * Stop only the callback-backed unit's resources, keeping shared module code + * available to other units/API consumers. STOP errors preserve active state. + * A plain library without lifecycle DIF retains disable/unload behavior. */ static int libsystemd_stop_library_service_internal(libsystemd_service_t service) { bool loaded = Dmod_IsModuleLoaded(service->exec); bool enabled = Dmod_IsModuleEnabled(service->exec); - - if (!loaded && !enabled) - { - return -ESRCH; - } - - DMOD_LOG_INFO("Unloading library service '%s' (module '%s')\n", service->unit_name, service->exec); - - if (enabled && !Dmod_DisableModule(service->exec, false)) - { - return -EIO; - } - - if (loaded && !Dmod_UnloadModule(service->exec, false)) - { - return -EIO; + if (!loaded && !enabled) { + bool active = service->library_started; + service->library_started = false; + return active ? 0 : -ESRCH; + } + if (!enabled) { + if (service->library_started) {service->library_started = false; return 0;} + return Dmod_UnloadModule(service->exec, false) ? 0 : -EIO; + } + Dmod_Context_t* module = Dmod_GetModuleContext(service->exec); + dmod_libsystemd_service_stop_t stop = Dmod_GetDifFunction(module, dmod_libsystemd_service_stop_sig); + if (stop != NULL) { + if (!service->library_started) return -ESRCH; + int rc = enabled ? stop(service->unit_name) : 0; + if (rc != 0) return rc; + service->library_started = false; + // The library may still serve other units or API consumers. The unit + // owns its callback state, not the lifetime of shared module code. + return 0; } - + if (enabled && !Dmod_DisableModule(service->exec, false)) return -EIO; + if (loaded && !Dmod_UnloadModule(service->exec, false)) return -EIO; return 0; } @@ -1410,7 +1378,12 @@ static void libsystemd_fill_status(libsystemd_service_t service, libsystemd_serv { if (service->type == LIBSYSTEMD_SERVICE_TYPE_LIBRARY) { - out_status->state = Dmod_IsModuleEnabled(service->exec) ? DMOSI_PROCESS_STATE_RUNNING : DMOSI_PROCESS_STATE_CREATED; + bool running = Dmod_IsModuleEnabled(service->exec); + Dmod_Context_t* module = Dmod_GetModuleContext(service->exec); + if (running && module != NULL && (Dmod_GetDifFunction(module, dmod_libsystemd_service_start_sig) != NULL || + Dmod_GetDifFunction(module, dmod_libsystemd_service_stop_sig) != NULL)) + running = running && service->library_started; + out_status->state = running ? DMOSI_PROCESS_STATE_RUNNING : DMOSI_PROCESS_STATE_CREATED; out_status->pid = 0; return; } @@ -2346,6 +2319,8 @@ static int libsystemd_build_service_from_ctx(dmini_context_t ctx, libsystemd_ser new_service->type = LIBSYSTEMD_SERVICE_TYPE_SIMPLE; new_service->restart_policy = LIBSYSTEMD_RESTART_NO; new_service->stop_timeout_ms = LIBSYSTEMD_DEFAULT_STOP_TIMEOUT_MS; + new_service->from_template = false; + new_service->library_started = false; new_service->exec = NULL; new_service->argc = 0; new_service->argv = NULL; @@ -2524,6 +2499,7 @@ static int libsystemd_instantiate_from_template(const char* units_dir, const cha } new_service->unit_name = unit_name; + new_service->from_template = true; *service = new_service; return 0; @@ -2594,6 +2570,30 @@ static libsystemd_service_t libsystemd_instantiate_service_on_demand(const char* return service; } +/* A removed/re-added device may retain its unit name but report a different + * absolute path. Re-expand an inactive library template with the new event; + * never reuse the old %v or change an active instance behind its callback. */ +static int libsystemd_refresh_library_instance(libsystemd_service_t service, const char* user_value) +{ + if (!service->from_template || service->type != LIBSYSTEMD_SERVICE_TYPE_LIBRARY || + service->library_started || user_value == NULL) return 0; + if (Dmod_IsModuleEnabled(service->exec)) { + Dmod_Context_t* module = Dmod_GetModuleContext(service->exec); + if (Dmod_GetDifFunction(module, dmod_libsystemd_service_start_sig) == NULL) return 0; + } + char* prefix = NULL; char* instance = NULL; + if (!libsystemd_split_instance_name(service->unit_name, &prefix, &instance)) return -ENOMEM; + libsystemd_template_t* tmpl = dmlist_find(g_services->templates, prefix, libsystemd_compare_template_by_prefix); + libsystemd_service_t fresh = NULL; + int rc = tmpl == NULL ? -ENOENT : libsystemd_instantiate_from_template(tmpl->dir_path, prefix, instance, user_value, &fresh); + Dmod_Free(prefix); Dmod_Free(instance); + if (rc != 0) return rc; + struct libsystemd_service old = *service; + *service = *fresh; *fresh = old; + libsystemd_destroy_service(fresh); + return 0; +} + /** * @brief Parse one rules file's (".ini" or ".rules", same INI syntax either way) "[class=...]" sections into @p rules * @@ -3158,8 +3158,9 @@ int dmod_deinit(void) * @param user_value Optional caller-supplied value, substituted for `%v` in * the template's keys if @p unit_name is instantiated * on demand (see above) - pass NULL if not needed. Has no - * effect if @p unit_name already exists in the registry, - * since only on-demand instantiation expands specifiers. + * effect on active/concrete units. An inactive library + * template instance is refreshed with a new device value + * so a re-added device cannot reuse a stale %v path. * * @retval 0 The service was found (or instantiated) and spawned successfully. * @retval -EINVAL @p unit_name was NULL. @@ -3188,6 +3189,7 @@ dmod_libsystemd_api_declaration(1.0, int, _start_service, ( const char* unit_nam } libsystemd_service_t service = libsystemd_find_service(g_services, unit_name); + bool existed = service != NULL; if (service == NULL) { service = libsystemd_instantiate_service_on_demand(unit_name, user_value); @@ -3197,6 +3199,8 @@ dmod_libsystemd_api_declaration(1.0, int, _start_service, ( const char* unit_nam return -ENOENT; } + int rc = existed ? libsystemd_refresh_library_instance(service, user_value) : 0; + if (rc != 0) return rc; return libsystemd_start_service_internal(service); } diff --git a/app/libsystemd/tests/CMakeLists.txt b/app/libsystemd/tests/CMakeLists.txt index 9e91831..c7c9650 100644 --- a/app/libsystemd/tests/CMakeLists.txt +++ b/app/libsystemd/tests/CMakeLists.txt @@ -5,6 +5,7 @@ set(DMOD_STACK_SIZE 1024) set(DMOD_PRIORITY 0) add_subdirectory(stoppable) +add_subdirectory(lifecycle) dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} libsystemd_test.c @@ -18,6 +19,7 @@ dmod_link_modules(${DMOD_MODULE_NAME} target_link_libraries(${DMOD_MODULE_NAME} libsystemd_if + dmtestlibrary_if ) # Absolute paths to the example unit files (shipped, user-facing) and to the diff --git a/app/libsystemd/tests/fixtures/lifecycle-rules/rtc.rules b/app/libsystemd/tests/fixtures/lifecycle-rules/rtc.rules new file mode 100644 index 0000000..d7794f7 --- /dev/null +++ b/app/libsystemd/tests/fixtures/lifecycle-rules/rtc.rules @@ -0,0 +1,2 @@ +[class=rtc] +start=clock@%name diff --git a/app/libsystemd/tests/fixtures/lifecycle/clock@.ini b/app/libsystemd/tests/fixtures/lifecycle/clock@.ini new file mode 100644 index 0000000..f306b94 --- /dev/null +++ b/app/libsystemd/tests/fixtures/lifecycle/clock@.ini @@ -0,0 +1,3 @@ +exec=dmtestlibrary +type=library +args=%v diff --git a/app/libsystemd/tests/fixtures/lifecycle/cold@.ini b/app/libsystemd/tests/fixtures/lifecycle/cold@.ini new file mode 100644 index 0000000..8f946de --- /dev/null +++ b/app/libsystemd/tests/fixtures/lifecycle/cold@.ini @@ -0,0 +1,3 @@ +exec=dmtestcold +type=library +args=%v diff --git a/app/libsystemd/tests/fixtures/lifecycle/plain@.ini b/app/libsystemd/tests/fixtures/lifecycle/plain@.ini new file mode 100644 index 0000000..517e478 --- /dev/null +++ b/app/libsystemd/tests/fixtures/lifecycle/plain@.ini @@ -0,0 +1,3 @@ +exec=dmtestplain +type=library +args=%v diff --git a/app/libsystemd/tests/libsystemd_test.c b/app/libsystemd/tests/libsystemd_test.c index e499416..99a30c6 100644 --- a/app/libsystemd/tests/libsystemd_test.c +++ b/app/libsystemd/tests/libsystemd_test.c @@ -1,6 +1,7 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmod_test.h" #include "libsystemd.h" +#include "dmtestlibrary.h" #include "dmosi.h" #include #include @@ -924,3 +925,82 @@ DMOD_TEST_STEP(set_stop_semaphore_registers_and_clears_for_calling_process) dmosi_semaphore_destroy(first); dmosi_semaphore_destroy(second); } + +DMOD_TEST_STEP(library_units_bind_devices_even_when_module_was_auto_enabled) +{ + /* Importing dmtestlibrary's API enables the library before its service. + * Starting a unit must still call its lifecycle handler, not EALREADY. */ + DMOD_TEST_EXPECT_TRUE(Dmod_IsModuleEnabled("dmtestlibrary")); + DMOD_TEST_EXPECT_EQ(libsystemd_scan(LIBSYSTEMD_TEST_FIXTURES_DIR "/lifecycle"),0); + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_added("rtc","pre","/clocks/pre"),-ENOENT); + DMOD_TEST_EXPECT_EQ(libsystemd_load_rules(LIBSYSTEMD_TEST_FIXTURES_DIR "/lifecycle-rules"),0); + DMOD_TEST_EXPECT_TRUE(dmtestlibrary_active("clock@pre")); + char path[128]; + DMOD_TEST_EXPECT_EQ(dmtestlibrary_path("clock@pre",path,sizeof(path)),0); + DMOD_TEST_EXPECT_EQ(strcmp(path,"/clocks/pre"),0); + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_added("rtc","other","/different/rtc"),0); + DMOD_TEST_EXPECT_TRUE(dmtestlibrary_active("clock@other")); + DMOD_TEST_EXPECT_EQ(libsystemd_start_service("clock@pre",NULL),-EALREADY); + DMOD_TEST_EXPECT_EQ(libsystemd_start_service("clock@bad","/reject"),-EINVAL); + libsystemd_service_status_t status; + DMOD_TEST_EXPECT_EQ(libsystemd_status("clock@bad",&status),0); + DMOD_TEST_EXPECT_EQ(status.state,DMOSI_PROCESS_STATE_CREATED); + DMOD_TEST_EXPECT_EQ(libsystemd_status("clock@other",&status),0); + DMOD_TEST_EXPECT_EQ(status.state,DMOSI_PROCESS_STATE_RUNNING); + DMOD_TEST_EXPECT_EQ(status.pid,0); + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_removed("rtc","pre"),0); + DMOD_TEST_EXPECT_FALSE(dmtestlibrary_active("clock@pre")); + DMOD_TEST_EXPECT_TRUE(dmtestlibrary_active("clock@other")); + DMOD_TEST_EXPECT_TRUE(Dmod_IsModuleEnabled("dmtestlibrary")); + DMOD_TEST_EXPECT_EQ(libsystemd_status("clock@pre",&status),0); + DMOD_TEST_EXPECT_EQ(status.state,DMOSI_PROCESS_STATE_CREATED); + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_added("rtc","pre","/clocks/moved"),0); + DMOD_TEST_EXPECT_TRUE(dmtestlibrary_active("clock@pre")); + DMOD_TEST_EXPECT_EQ(dmtestlibrary_path("clock@pre",path,sizeof(path)),0); + DMOD_TEST_EXPECT_EQ(strcmp(path,"/clocks/moved"),0); + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_removed("rtc","pre"),0); + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_removed("rtc","other"),0); + DMOD_TEST_EXPECT_FALSE(dmtestlibrary_active("clock@other")); + DMOD_TEST_EXPECT_TRUE(Dmod_IsModuleEnabled("dmtestlibrary")); +} + +DMOD_TEST_STEP(library_callbacks_start_on_first_load_and_on_restart) +{ + DMOD_TEST_EXPECT_FALSE(Dmod_IsModuleEnabled("dmtestcold")); + DMOD_TEST_EXPECT_EQ(libsystemd_scan(LIBSYSTEMD_TEST_FIXTURES_DIR "/lifecycle"),0); + DMOD_TEST_EXPECT_EQ(libsystemd_start_service("cold@rtc","/cold/clock"),0); + libsystemd_service_status_t status; + DMOD_TEST_EXPECT_EQ(libsystemd_status("cold@rtc",&status),0); + DMOD_TEST_EXPECT_EQ(status.state,DMOSI_PROCESS_STATE_RUNNING); + DMOD_TEST_EXPECT_EQ(libsystemd_stop_service("cold@rtc"),0); + DMOD_TEST_EXPECT_TRUE(Dmod_IsModuleEnabled("dmtestcold")); + DMOD_TEST_EXPECT_EQ(libsystemd_status("cold@rtc",&status),0); + DMOD_TEST_EXPECT_EQ(status.state,DMOSI_PROCESS_STATE_CREATED); + DMOD_TEST_EXPECT_EQ(libsystemd_start_service("cold@rtc",NULL),0); + DMOD_TEST_EXPECT_EQ(libsystemd_status("cold@rtc",&status),0); + DMOD_TEST_EXPECT_EQ(status.state,DMOSI_PROCESS_STATE_RUNNING); + DMOD_TEST_EXPECT_EQ(libsystemd_stop_service("cold@rtc"),0); +} + +DMOD_TEST_STEP(plain_library_retains_enable_disable_unload_and_restart) +{ + DMOD_TEST_EXPECT_FALSE(Dmod_IsModuleLoaded("dmtestplain")); + DMOD_TEST_EXPECT_EQ(libsystemd_scan(LIBSYSTEMD_TEST_FIXTURES_DIR "/lifecycle"), 0); + libsystemd_service_status_t status; + for (int cycle = 0; cycle < 2; ++cycle) + { + DMOD_TEST_EXPECT_EQ(libsystemd_start_service("plain@legacy", "/ignored/argument"), 0); + DMOD_TEST_EXPECT_TRUE(Dmod_IsModuleLoaded("dmtestplain")); + DMOD_TEST_EXPECT_TRUE(Dmod_IsModuleEnabled("dmtestplain")); + DMOD_TEST_EXPECT_EQ(libsystemd_status("plain@legacy", &status), 0); + DMOD_TEST_EXPECT_EQ(status.state, DMOSI_PROCESS_STATE_RUNNING); + DMOD_TEST_EXPECT_EQ(status.pid, 0); + DMOD_TEST_EXPECT_EQ(libsystemd_start_service("plain@legacy", NULL), -EALREADY); + DMOD_TEST_EXPECT_EQ(libsystemd_stop_service("plain@legacy"), 0); + DMOD_TEST_EXPECT_FALSE(Dmod_IsModuleEnabled("dmtestplain")); + DMOD_TEST_EXPECT_FALSE(Dmod_IsModuleLoaded("dmtestplain")); + DMOD_TEST_EXPECT_EQ(libsystemd_status("plain@legacy", &status), 0); + DMOD_TEST_EXPECT_EQ(status.state, DMOSI_PROCESS_STATE_CREATED); + DMOD_TEST_EXPECT_EQ(libsystemd_stop_service("plain@legacy"), -ESRCH); + } +} diff --git a/app/libsystemd/tests/lifecycle/CMakeLists.txt b/app/libsystemd/tests/lifecycle/CMakeLists.txt new file mode 100644 index 0000000..e5fc696 --- /dev/null +++ b/app/libsystemd/tests/lifecycle/CMakeLists.txt @@ -0,0 +1,18 @@ +set(DMOD_MODULE_NAME dmtestlibrary) +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dmtestlibrary.dmr) +set(DMOD_DIF_IMPLS libsystemd) +dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} library.c) +target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(${DMOD_MODULE_NAME} libsystemd_if) + +set(DMOD_MODULE_NAME dmtestcold) +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dmtestcold.dmr) +dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} cold.c) +target_link_libraries(${DMOD_MODULE_NAME} libsystemd_if) + +# No lifecycle DIF and no imported API: exercise the legacy load/enable and +# disable/unload path independently of shared consumer dependencies. +set(DMOD_MODULE_NAME dmtestplain) +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dmtestplain.dmr) +set(DMOD_DIF_IMPLS "") +dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} plain.c) diff --git a/app/libsystemd/tests/lifecycle/cold.c b/app/libsystemd/tests/lifecycle/cold.c new file mode 100644 index 0000000..dc08baf --- /dev/null +++ b/app/libsystemd/tests/lifecycle/cold.c @@ -0,0 +1,22 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "libsystemd_service.h" +#include +#include +static char* active; +dmod_libsystemd_dif_api_declaration(1.0, dmtestcold, int, _service_start, + (const char* unit,int argc,const char* const argv[])) +{ + if(argc!=2 || strcmp(argv[1],"/cold/clock")!=0)return -EINVAL; + if(active!=NULL)return -EALREADY; + active=Dmod_Malloc(strlen(unit)+1); + if(active==NULL)return -ENOMEM; + strcpy(active,unit);return 0; +} +dmod_libsystemd_dif_api_declaration(1.0, dmtestcold, int, _service_stop, (const char* unit)) +{ + if(active==NULL || strcmp(active,unit)!=0)return -ENOENT; + Dmod_Free(active);active=NULL;return 0; +} +int dmod_init(const Dmod_Config_t* config) {(void)config;return 0;} +int dmod_deinit(void) {Dmod_Free(active);active=NULL;return 0;} diff --git a/app/libsystemd/tests/lifecycle/dmtestcold.dmr b/app/libsystemd/tests/lifecycle/dmtestcold.dmr new file mode 100644 index 0000000..e05b30b --- /dev/null +++ b/app/libsystemd/tests/lifecycle/dmtestcold.dmr @@ -0,0 +1,6 @@ +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/app/libsystemd/tests/lifecycle/${module}.dmr] +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] diff --git a/app/libsystemd/tests/lifecycle/dmtestlibrary.dmr b/app/libsystemd/tests/lifecycle/dmtestlibrary.dmr new file mode 100644 index 0000000..e05b30b --- /dev/null +++ b/app/libsystemd/tests/lifecycle/dmtestlibrary.dmr @@ -0,0 +1,6 @@ +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/app/libsystemd/tests/lifecycle/${module}.dmr] +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] diff --git a/app/libsystemd/tests/lifecycle/dmtestlibrary.h b/app/libsystemd/tests/lifecycle/dmtestlibrary.h new file mode 100644 index 0000000..b01be6c --- /dev/null +++ b/app/libsystemd/tests/lifecycle/dmtestlibrary.h @@ -0,0 +1,8 @@ +#ifndef DMTESTLIBRARY_H +#define DMTESTLIBRARY_H +#include +#include +#include "dmtestlibrary_defs.h" +dmod_dmtestlibrary_api(1.0, bool, _active, (const char* unit)); +dmod_dmtestlibrary_api(1.0, int, _path, (const char* unit, char* path, size_t capacity)); +#endif diff --git a/app/libsystemd/tests/lifecycle/dmtestplain.dmr b/app/libsystemd/tests/lifecycle/dmtestplain.dmr new file mode 100644 index 0000000..e05b30b --- /dev/null +++ b/app/libsystemd/tests/lifecycle/dmtestplain.dmr @@ -0,0 +1,6 @@ +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/app/libsystemd/tests/lifecycle/${module}.dmr] +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] diff --git a/app/libsystemd/tests/lifecycle/library.c b/app/libsystemd/tests/lifecycle/library.c new file mode 100644 index 0000000..edb7e17 --- /dev/null +++ b/app/libsystemd/tests/lifecycle/library.c @@ -0,0 +1,42 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmtestlibrary.h" +#include "libsystemd_service.h" +#include +#include +struct instance {char unit[96]; char path[128];}; +static struct instance* instances[4]; +dmod_dmtestlibrary_api_declaration(1.0, bool, _active, (const char* unit)) +{ + for(unsigned i=0;i<4;++i) if(instances[i]!=NULL && strcmp(instances[i]->unit,unit)==0)return true; + return false; +} +dmod_dmtestlibrary_api_declaration(1.0, int, _path, (const char* unit,char* path,size_t capacity)) +{ + for(unsigned i=0;i<4;++i) if(instances[i]!=NULL && strcmp(instances[i]->unit,unit)==0) { + if(strlen(instances[i]->path)+1>capacity)return -ENOSPC; + strcpy(path,instances[i]->path); return 0; + } + return -ENOENT; +} +dmod_libsystemd_dif_api_declaration(1.0, dmtestlibrary, int, _service_start, + (const char* unit,int argc,const char* const argv[])) +{ + if(argc!=2 || argv==NULL || strcmp(argv[1],"/reject")==0)return -EINVAL; + if(strlen(unit)>=sizeof(((struct instance*)0)->unit) || strlen(argv[1])>=sizeof(((struct instance*)0)->path))return -ENAMETOOLONG; + for(unsigned i=0;i<4;++i) if(instances[i]==NULL) { + instances[i]=Dmod_Malloc(sizeof(*instances[i])); + if(instances[i]==NULL)return -ENOMEM; + strcpy(instances[i]->unit,unit); strcpy(instances[i]->path,argv[1]); return 0; + } + return -ENOSPC; +} +dmod_libsystemd_dif_api_declaration(1.0, dmtestlibrary, int, _service_stop, (const char* unit)) +{ + for(unsigned i=0;i<4;++i) if(instances[i]!=NULL && strcmp(instances[i]->unit,unit)==0) { + Dmod_Free(instances[i]);instances[i]=NULL; return 0; + } + return -ENOENT; +} +int dmod_init(const Dmod_Config_t* config) {(void)config;memset(instances,0,sizeof(instances));return 0;} +int dmod_deinit(void) {for(unsigned i=0;i<4;++i)Dmod_Free(instances[i]);return 0;} diff --git a/app/libsystemd/tests/lifecycle/plain.c b/app/libsystemd/tests/lifecycle/plain.c new file mode 100644 index 0000000..57bf1c5 --- /dev/null +++ b/app/libsystemd/tests/lifecycle/plain.c @@ -0,0 +1,17 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include + +static void* allocation; +int dmod_init(const Dmod_Config_t* config) +{ + (void)config; + allocation = Dmod_Malloc(1); + return allocation == NULL ? -ENOMEM : 0; +} +int dmod_deinit(void) +{ + Dmod_Free(allocation); + allocation = NULL; + return 0; +}