Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions app/libsystemd/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<prefix>@<instance>`-shaped
and `<prefix>@.ini` exists in the last-scanned units directory, it is
Expand All @@ -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
Expand All @@ -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).

Expand All @@ -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)`
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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).
86 changes: 44 additions & 42 deletions app/libsystemd/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 46 additions & 0 deletions app/libsystemd/docs/library-service-validation.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 23 additions & 0 deletions app/libsystemd/include/libsystemd_service.h
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion app/libsystemd/include/libsystemd_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
Loading
Loading